From 130ff6813253cfaf2bb114f934db49ff2815d176 Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Thu, 20 Aug 2015 11:52:32 -1000 Subject: [PATCH 001/649] Throw TypeError when passing booleans to arrow.get Before: >>> arrow.get(False) >>> arrow.get(True) After: >>> arrow.get(False) TypeError: Can't parse single argument type of '' >>> arrow.get(True) TypeError: Can't parse single argument type of '' --- arrow/util.py | 2 ++ tests/factory_tests.py | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/arrow/util.py b/arrow/util.py index d0d060f7d..b1a34b916 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -23,6 +23,8 @@ def _total_seconds_26(td): total_seconds = _total_seconds_27 def is_timestamp(value): + if type(value) == bool: + return False try: float(value) return True diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 35d60edbc..7568c95b3 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -95,6 +95,14 @@ def test_one_arg_other(self): with assertRaises(TypeError): self.factory.get(object()) + def test_one_arg_bool(self): + + with assertRaises(TypeError): + self.factory.get(False) + + with assertRaises(TypeError): + self.factory.get(True) + def test_two_args_datetime_tzinfo(self): result = self.factory.get(datetime(2013, 1, 1), tz.gettz('US/Pacific')) @@ -190,4 +198,3 @@ def test_tzinfo(self): def test_tz_str(self): assertDtEqual(self.factory.now('EST'), datetime.now(tz.gettz('EST'))) - From 4854aac6b6f6cecaf7b3ff10ee741d79a4073afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Wed, 14 Oct 2015 17:58:03 +0200 Subject: [PATCH 002/649] Add SwissGerman localization --- arrow/locales.py | 43 ++++++++++++++++++++++++++++++++++++++++++ tests/locales_tests.py | 12 ++++++++++++ 2 files changed, 55 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 9c70bdccb..ac719ae33 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -838,6 +838,49 @@ class AustriaLocale(Locale): '', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So' ] + +class SwissLocale(Locale): + + names = ['de', 'de_ch'] + + past = 'vor {0}' + future = 'in {0}' + + timeframes = { + 'now': 'gerade eben', + 'seconds': 'Sekunden', + 'minute': 'einer Minute', + 'minutes': '{0} Minuten', + 'hour': 'einer Stunde', + 'hours': '{0} Stunden', + 'day': 'einem Tag', + 'days': '{0} Tage', + 'month': 'einem Monat', + 'months': '{0} Monaten', + 'year': 'einem Jahr', + 'years': '{0} Jahren', + } + + month_names = [ + '', 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', + 'August', 'September', 'Oktober', 'November', 'Dezember' + ] + + month_abbreviations = [ + '', 'Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', + 'Okt', 'Nov', 'Dez' + ] + + day_names = [ + '', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', + 'Samstag', 'Sonntag' + ] + + day_abbreviations = [ + '', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So' + ] + + class NorwegianLocale(Locale): names = ['nb', 'nb_no'] diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 42a5a6fab..db6c3dea2 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -309,3 +309,15 @@ def test_format_relative_future(self): # Not currently implemented def test_ordinal_number(self): assertEqual(self.locale.ordinal_number(1), '1') + + +class SwissLocalesTests(Chai): + + def test_ordinal_number(self): + self.locale = locales.SwissLocale() + + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + + assertEqual(self.locale._format_timeframe('minute', 1), 'einer Minute') + assertEqual(self.locale._format_timeframe('hour', 1), 'einer Stunde') + assertEqual(self.locale.day_abbreviation(dt.isoweekday()), 'Sa') \ No newline at end of file From 15d903ab82ef101776dcc4bc93cff48716fd0228 Mon Sep 17 00:00:00 2001 From: sipp11 Date: Thu, 29 Oct 2015 01:59:23 +0700 Subject: [PATCH 003/649] Separate replace & shift function according to PR Request from issue #220 --- arrow/arrow.py | 41 +++++++++++++++------ tests/arrow_tests.py | 86 +++++++++++++++++++++++--------------------- 2 files changed, 76 insertions(+), 51 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 07947c116..7a5c3bfe2 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -378,11 +378,6 @@ def replace(self, **kwargs): >>> arw.replace(year=2014, month=6) - Use plural property names to shift their current value relatively: - - >>> arw.replace(years=1, months=-1) - - You can also provide a timezone expression can also be replaced: >>> arw.replace(tzinfo=tz.tzlocal()) @@ -398,21 +393,17 @@ def replace(self, **kwargs): ''' absolute_kwargs = {} - relative_kwargs = {} for key, value in kwargs.items(): if key in self._ATTRS: absolute_kwargs[key] = value - elif key in self._ATTRS_PLURAL or key == 'weeks': - relative_kwargs[key] = value elif key == 'week': raise AttributeError('setting absolute week is not supported') elif key !='tzinfo': raise AttributeError() current = self._datetime.replace(**absolute_kwargs) - current += relativedelta(**relative_kwargs) tzinfo = kwargs.get('tzinfo') @@ -422,9 +413,37 @@ def replace(self, **kwargs): return self.fromdatetime(current) + def shift(self, **kwargs): + ''' Returns a new :class:`Arrow ` object with + attributes updated according to inputs. + + Use plural property names to shift their current value relatively: + + >>> import arrow + >>> arw = arrow.utcnow() + >>> arw + + >>> arw.shift(years=1, months=-1) + + + ''' + + relative_kwargs = {} + + for key, value in kwargs.items(): + + if key in self._ATTRS_PLURAL or key == 'weeks': + relative_kwargs[key] = value + else: + raise AttributeError() + + current = self._datetime + relativedelta(**relative_kwargs) + + return self.fromdatetime(current) + def to(self, tz): - ''' Returns a new :class:`Arrow ` object, converted to the target - timezone. + ''' Returns a new :class:`Arrow ` object, converted + to the target timezone. :param tz: an expression representing a timezone. diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 882479573..16bde5ab9 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -205,7 +205,7 @@ def test_ne(self): def test_gt(self): - arrow_cmp = self.arrow.replace(minutes=1) + arrow_cmp = self.arrow.shift(minutes=1) assertFalse(self.arrow > self.arrow) assertFalse(self.arrow > self.arrow.datetime) @@ -226,7 +226,7 @@ def test_ge(self): def test_lt(self): - arrow_cmp = self.arrow.replace(minutes=1) + arrow_cmp = self.arrow.shift(minutes=1) assertFalse(self.arrow < self.arrow) assertFalse(self.arrow < self.arrow.datetime) @@ -440,7 +440,7 @@ def test_not_attr(self): with assertRaises(AttributeError): arrow.Arrow.utcnow().replace(abc=1) - def test_replace_absolute(self): + def test_replace(self): arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) @@ -451,32 +451,6 @@ def test_replace_absolute(self): assertEqual(arw.replace(minute=1), arrow.Arrow(2013, 5, 5, 12, 1, 45)) assertEqual(arw.replace(second=1), arrow.Arrow(2013, 5, 5, 12, 30, 1)) - def test_replace_relative(self): - - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) - - assertEqual(arw.replace(years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) - assertEqual(arw.replace(months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) - assertEqual(arw.replace(weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) - assertEqual(arw.replace(days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - assertEqual(arw.replace(hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) - assertEqual(arw.replace(minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) - assertEqual(arw.replace(seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) - - def test_replace_relative_negative(self): - - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) - - assertEqual(arw.replace(years=-1), arrow.Arrow(2012, 5, 5, 12, 30, 45)) - assertEqual(arw.replace(months=-1), arrow.Arrow(2013, 4, 5, 12, 30, 45)) - assertEqual(arw.replace(weeks=-1), arrow.Arrow(2013, 4, 28, 12, 30, 45)) - assertEqual(arw.replace(days=-1), arrow.Arrow(2013, 5, 4, 12, 30, 45)) - assertEqual(arw.replace(hours=-1), arrow.Arrow(2013, 5, 5, 11, 30, 45)) - assertEqual(arw.replace(minutes=-1), arrow.Arrow(2013, 5, 5, 12, 29, 45)) - assertEqual(arw.replace(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) - assertEqual(arw.replace(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999)) - - def test_replace_tzinfo(self): arw = arrow.Arrow.utcnow().to('US/Eastern') @@ -495,6 +469,38 @@ def test_replace_other_kwargs(self): with assertRaises(AttributeError): arrow.utcnow().replace(abc='def') +class ArrowShiftTests(Chai): + + def test_not_attr(self): + + with assertRaises(AttributeError): + arrow.Arrow.utcnow().replace(abc=1) + + def test_shift(self): + + arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) + + assertEqual(arw.shift(years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) + assertEqual(arw.shift(months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) + assertEqual(arw.shift(weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) + assertEqual(arw.shift(days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + assertEqual(arw.shift(hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) + assertEqual(arw.shift(minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) + assertEqual(arw.shift(seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) + + def test_shift_negative(self): + + arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) + + assertEqual(arw.shift(years=-1), arrow.Arrow(2012, 5, 5, 12, 30, 45)) + assertEqual(arw.shift(months=-1), arrow.Arrow(2013, 4, 5, 12, 30, 45)) + assertEqual(arw.shift(weeks=-1), arrow.Arrow(2013, 4, 28, 12, 30, 45)) + assertEqual(arw.shift(days=-1), arrow.Arrow(2013, 5, 4, 12, 30, 45)) + assertEqual(arw.shift(hours=-1), arrow.Arrow(2013, 5, 5, 11, 30, 45)) + assertEqual(arw.shift(minutes=-1), arrow.Arrow(2013, 5, 5, 12, 29, 45)) + assertEqual(arw.shift(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) + assertEqual(arw.shift(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999)) + class ArrowRangeTests(Chai): @@ -909,7 +915,7 @@ def setUp(self): def test_seconds(self): - later = self.now.replace(seconds=10) + later = self.now.shift(seconds=10) assertEqual(self.now.humanize(later), 'seconds ago') assertEqual(later.humanize(self.now), 'in seconds') @@ -919,7 +925,7 @@ def test_seconds(self): def test_minute(self): - later = self.now.replace(minutes=1) + later = self.now.shift(minutes=1) assertEqual(self.now.humanize(later), 'a minute ago') assertEqual(later.humanize(self.now), 'in a minute') @@ -930,7 +936,7 @@ def test_minute(self): def test_minutes(self): - later = self.now.replace(minutes=2) + later = self.now.shift(minutes=2) assertEqual(self.now.humanize(later), '2 minutes ago') assertEqual(later.humanize(self.now), 'in 2 minutes') @@ -940,7 +946,7 @@ def test_minutes(self): def test_hour(self): - later = self.now.replace(hours=1) + later = self.now.shift(hours=1) assertEqual(self.now.humanize(later), 'an hour ago') assertEqual(later.humanize(self.now), 'in an hour') @@ -950,7 +956,7 @@ def test_hour(self): def test_hours(self): - later = self.now.replace(hours=2) + later = self.now.shift(hours=2) assertEqual(self.now.humanize(later), '2 hours ago') assertEqual(later.humanize(self.now), 'in 2 hours') @@ -960,7 +966,7 @@ def test_hours(self): def test_day(self): - later = self.now.replace(days=1) + later = self.now.shift(days=1) assertEqual(self.now.humanize(later), 'a day ago') assertEqual(later.humanize(self.now), 'in a day') @@ -970,7 +976,7 @@ def test_day(self): def test_days(self): - later = self.now.replace(days=2) + later = self.now.shift(days=2) assertEqual(self.now.humanize(later), '2 days ago') assertEqual(later.humanize(self.now), 'in 2 days') @@ -980,7 +986,7 @@ def test_days(self): def test_month(self): - later = self.now.replace(months=1) + later = self.now.shift(months=1) assertEqual(self.now.humanize(later), 'a month ago') assertEqual(later.humanize(self.now), 'in a month') @@ -990,7 +996,7 @@ def test_month(self): def test_months(self): - later = self.now.replace(months=2) + later = self.now.shift(months=2) assertEqual(self.now.humanize(later), '2 months ago') assertEqual(later.humanize(self.now), 'in 2 months') @@ -1000,7 +1006,7 @@ def test_months(self): def test_year(self): - later = self.now.replace(years=1) + later = self.now.shift(years=1) assertEqual(self.now.humanize(later), 'a year ago') assertEqual(later.humanize(self.now), 'in a year') @@ -1010,7 +1016,7 @@ def test_year(self): def test_years(self): - later = self.now.replace(years=2) + later = self.now.shift(years=2) assertEqual(self.now.humanize(later), '2 years ago') assertEqual(later.humanize(self.now), 'in 2 years') From b6487ab66a94b57d633d7f32c0792dcd4c4a4d9e Mon Sep 17 00:00:00 2001 From: Dillon Dixon Date: Fri, 11 Dec 2015 11:52:37 -0800 Subject: [PATCH 004/649] Added support for escaping in parsing formats --- arrow/parser.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 2c204aeeb..1c89a721b 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -5,7 +5,6 @@ from datetime import datetime from dateutil import tz import re - from arrow import locales @@ -16,6 +15,7 @@ class ParserError(RuntimeError): class DateTimeParser(object): _FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X)') + _ESCAPE_RE = re.compile('\[[^\[\]]*\]') _ONE_THROUGH_SIX_DIGIT_RE = re.compile('\d{1,6}') _ONE_THROUGH_FIVE_DIGIT_RE = re.compile('\d{1,5}') @@ -123,10 +123,16 @@ def parse(self, string, fmt): # we construct a new string by replacing each # token by its pattern: # 'YYYY-MM-DD' -> '(?P\d{4})-(?P\d{2})-(?P
\d{2})' - fmt_pattern = fmt tokens = [] offset = 0 - for m in self._FORMAT_RE.finditer(fmt): + + # Extract the bracketed expressions to be reinserted later. + escaped_fmt = re.sub(self._ESCAPE_RE, "#" , fmt) + escaped_data = re.findall(self._ESCAPE_RE, fmt) + + fmt_pattern = escaped_fmt + + for m in self._FORMAT_RE.finditer(escaped_fmt): token = m.group(0) try: input_re = self._input_re_map[token] @@ -140,9 +146,14 @@ def parse(self, string, fmt): # are returned in the order found by finditer. fmt_pattern = fmt_pattern[:m.start() + offset] + input_pattern + fmt_pattern[m.end() + offset:] offset += len(input_pattern) - (m.end() - m.start()) - match = re.search(fmt_pattern, string, flags=re.IGNORECASE) + + final_fmt_pattern = "" + for pattern_part, escaped in zip(fmt_pattern.split("#"), escaped_data + [""]): + final_fmt_pattern += pattern_part + escaped[1:-1] + + match = re.search(final_fmt_pattern, string, flags=re.IGNORECASE) if match is None: - raise ParserError('Failed to match \'{0}\' when parsing \'{1}\''.format(fmt_pattern, string)) + raise ParserError('Failed to match \'{0}\' when parsing \'{1}\''.format(final_fmt_pattern, string)) parts = {} for token in tokens: if token == 'Do': From dabdbe153a0fdb5448572846cbad04253e13bccb Mon Sep 17 00:00:00 2001 From: Dillon Dixon Date: Fri, 11 Dec 2015 13:03:19 -0800 Subject: [PATCH 005/649] Added tests for the feature --- tests/parser_tests.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index ed34863e2..94c5b0bab 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -675,7 +675,7 @@ def test_parse_search(self): self.parser.parse('Today is 25 of September of 2003', 'DD of MMMM of YYYY'), datetime(2003, 9, 25)) - def test_parse_seach_with_numbers(self): + def test_parse_search_with_numbers(self): assertEqual( self.parser.parse('2000 people met the 2012-01-01 12:05:10', 'YYYY-MM-DD HH:mm:ss'), @@ -685,13 +685,13 @@ def test_parse_seach_with_numbers(self): self.parser.parse('Call 01-02-03 on 79-01-01 12:05:10', 'YY-MM-DD HH:mm:ss'), datetime(1979, 1, 1, 12, 5, 10)) - def test_parse_seach_with_names(self): + def test_parse_search_with_names(self): assertEqual( self.parser.parse('June was born in May 1980', 'MMMM YYYY'), datetime(1980, 5, 1)) - def test_parse_seach_locale_with_names(self): + def test_parse_search_locale_with_names(self): p = parser.DateTimeParser('sv_se') assertEqual( @@ -702,7 +702,24 @@ def test_parse_seach_locale_with_names(self): p.parse('Jag föddes den 25 Augusti 1975', 'DD MMMM YYYY'), datetime(1975, 8, 25)) - def test_parse_seach_fails(self): + def test_parse_search_fails(self): with assertRaises(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" + assertEqual( + self.parser.parse("Thursday, December 10, 2015 at 5:09pm", format), + datetime(2015, 12, 10, 17, 9)) + + format = "[MMMM] M D, YYYY [at] h:mma" + assertEqual( + self.parser.parse("MMMM 12 10, 2015 at 5:09pm", format), + datetime(2015, 12, 10, 17, 9)) + + format = "[It happened on] MMMM Do [in the year] YYYY" + assertEqual( + self.parser.parse("It happened on November 25th in the year 1990", format), + datetime(1990, 11, 25)) From 77c6e854952e0f50a24a3c29ae1f4146a36071f9 Mon Sep 17 00:00:00 2001 From: Dillon Dixon Date: Fri, 11 Dec 2015 23:04:08 -0800 Subject: [PATCH 006/649] Fixed a bug in rebuilding the escaped regular expression --- arrow/parser.py | 11 +++++++++-- tests/parser_tests.py | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 1c89a721b..c87d93a63 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -148,8 +148,15 @@ def parse(self, string, fmt): offset += len(input_pattern) - (m.end() - m.start()) final_fmt_pattern = "" - for pattern_part, escaped in zip(fmt_pattern.split("#"), escaped_data + [""]): - final_fmt_pattern += pattern_part + escaped[1:-1] + + a = fmt_pattern.split("#") + b = escaped_data + + for i in range(max(len(a), len(b))): + if i < len(a): + final_fmt_pattern += a[i] + if i < len(b): + final_fmt_pattern += b[i][1:-1] match = re.search(final_fmt_pattern, string, flags=re.IGNORECASE) if match is None: diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 94c5b0bab..66ee9ce2f 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -719,7 +719,7 @@ def test_escape(self): self.parser.parse("MMMM 12 10, 2015 at 5:09pm", format), datetime(2015, 12, 10, 17, 9)) - format = "[It happened on] MMMM Do [in the year] YYYY" + format = "[It happened on] MMMM Do [in the] [year] YYYY [a long time ago]" assertEqual( - self.parser.parse("It happened on November 25th in the year 1990", format), + self.parser.parse("It happened on November 25th in the year 1990 a long time ago", format), datetime(1990, 11, 25)) From 3a8bee4a9ae78f547e46d58bf3ecd96e26f288a5 Mon Sep 17 00:00:00 2001 From: Dillon Dixon Date: Fri, 11 Dec 2015 23:10:58 -0800 Subject: [PATCH 007/649] Fixed coverage --- tests/parser_tests.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 66ee9ce2f..632af1fe1 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -719,6 +719,11 @@ def test_escape(self): self.parser.parse("MMMM 12 10, 2015 at 5:09pm", format), datetime(2015, 12, 10, 17, 9)) + format = "[It happened on] MMMM Do [in the year] YYYY [a long time ago]" + assertEqual( + self.parser.parse("It happened on November 25th in the year 1990 a long time ago", format), + datetime(1990, 11, 25)) + format = "[It happened on] MMMM Do [in the] [year] YYYY [a long time ago]" assertEqual( self.parser.parse("It happened on November 25th in the year 1990 a long time ago", format), From 81eed5283825a4b0d64072d7070da2668a4aff84 Mon Sep 17 00:00:00 2001 From: Dillon Dixon Date: Fri, 11 Dec 2015 23:59:12 -0800 Subject: [PATCH 008/649] Removed affectively dead code discovered during coverage testing --- arrow/parser.py | 7 +++---- tests/parser_tests.py | 7 ++++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index c87d93a63..424b28298 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -148,13 +148,12 @@ def parse(self, string, fmt): offset += len(input_pattern) - (m.end() - m.start()) final_fmt_pattern = "" - a = fmt_pattern.split("#") b = escaped_data - for i in range(max(len(a), len(b))): - if i < len(a): - final_fmt_pattern += a[i] + # Due to the way Python splits, 'a' will always be longer + for i in range(len(a)): + final_fmt_pattern += a[i] if i < len(b): final_fmt_pattern += b[i][1:-1] diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 632af1fe1..a0905c0d3 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -724,7 +724,12 @@ def test_escape(self): self.parser.parse("It happened on November 25th in the year 1990 a long time ago", format), datetime(1990, 11, 25)) - format = "[It happened on] MMMM Do [in the] [year] YYYY [a long time ago]" + format = "[It happened on] MMMM Do [in the][ year] YYYY [a long time ago]" assertEqual( self.parser.parse("It happened on November 25th in the year 1990 a long time ago", format), datetime(1990, 11, 25)) + + format = "[I'm][ entirely][ escaped,][ weee!]" + assertEqual( + self.parser.parse("I'm entirely escaped, weee!", format), + datetime(1, 1, 1)) From 47e715845d49243fbd0f065f690f8d2ce6ab440e Mon Sep 17 00:00:00 2001 From: Ben Mathes Date: Mon, 4 Jan 2016 13:12:26 -0800 Subject: [PATCH 009/649] adding arrow.quarter property --- arrow/arrow.py | 4 ++++ tests/arrow_tests.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/arrow/arrow.py b/arrow/arrow.py index d8c63857a..bce3c69ae 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -45,6 +45,7 @@ class Arrow(object): _ATTRS = ['year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond'] _ATTRS_PLURAL = ['{0}s'.format(a) for a in _ATTRS] + _MONTHS_PER_QUARTER = 3 def __init__(self, year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None): @@ -306,6 +307,9 @@ def __getattr__(self, name): if name == 'week': return self.isocalendar()[1] + if name == 'quarter': + return int(self.month/self._MONTHS_PER_QUARTER) + 1 + if not name.startswith('_'): value = getattr(self._datetime, name, None) diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index d0d717d3e..9e432e0c4 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -160,6 +160,16 @@ def test_getattr_week(self): assertEqual(self.arrow.week, 1) + def test_getattr_quarter(self): + q1 = arrow.Arrow(2013, 1, 1) + q2 = arrow.Arrow(2013, 4, 1) + q3 = arrow.Arrow(2013, 8, 1) + q4 = arrow.Arrow(2013, 10, 1) + assertEqual(q1.quarter, 1) + assertEqual(q2.quarter, 2) + assertEqual(q3.quarter, 3) + assertEqual(q4.quarter, 4) + def test_getattr_dt_value(self): assertEqual(self.arrow.year, 2013) From 81259e744a2ae3f27bba0bd4131b33a1e02d52ee Mon Sep 17 00:00:00 2001 From: Ben Mathes Date: Mon, 4 Jan 2016 15:16:18 -0800 Subject: [PATCH 010/649] adds relative replace values for quarters --- arrow/arrow.py | 13 ++++++++++--- tests/arrow_tests.py | 4 +++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index bce3c69ae..224dc93ff 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -408,13 +408,20 @@ def replace(self, **kwargs): if key in self._ATTRS: absolute_kwargs[key] = value - elif key in self._ATTRS_PLURAL or key == 'weeks': + elif key in self._ATTRS_PLURAL or key in ['weeks', 'quarters']: relative_kwargs[key] = value - elif key == 'week': - raise AttributeError('setting absolute week is not supported') + elif key in ['week', 'quarter']: + raise AttributeError('setting absolute {} is not supported'.format(key)) elif key !='tzinfo': raise AttributeError() + # core datetime does not support quarters, translate to months. + if 'quarters' in relative_kwargs.keys(): + if relative_kwargs.get('months') is None: + relative_kwargs['months'] = 0 + relative_kwargs['months'] += (value * self._MONTHS_PER_QUARTER) + relative_kwargs.pop('quarters') + current = self._datetime.replace(**absolute_kwargs) current += relativedelta(**relative_kwargs) diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 9e432e0c4..4f8e4ee2b 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -168,7 +168,7 @@ def test_getattr_quarter(self): assertEqual(q1.quarter, 1) assertEqual(q2.quarter, 2) assertEqual(q3.quarter, 3) - assertEqual(q4.quarter, 4) + assertEqual(q4.quarter, 4) def test_getattr_dt_value(self): @@ -465,6 +465,8 @@ def test_replace_relative(self): arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) assertEqual(arw.replace(years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) + assertEqual(arw.replace(quarters=1), arrow.Arrow(2013, 8, 5, 12, 30, 45)) + assertEqual(arw.replace(quarters=1, months=1), arrow.Arrow(2013, 9, 5, 12, 30, 45)) assertEqual(arw.replace(months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) assertEqual(arw.replace(weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) assertEqual(arw.replace(days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) From c63e908fa19dfa48169f058197fcbd49be682c06 Mon Sep 17 00:00:00 2001 From: Taylor Edmiston Date: Mon, 4 Jan 2016 23:48:03 -0500 Subject: [PATCH 011/649] Fix typo in docs for using arrow.ArrowFactory --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 6e259c069..1b37949ff 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -364,7 +364,7 @@ Then get and use a factory for it: .. code-block:: python - >>> factory = arrow.Factory(CustomArrow) + >>> factory = arrow.ArrowFactory(CustomArrow) >>> custom = factory.utcnow() >>> custom >>> From c3de029447d20fb0cf046dcf03e3c012f8293b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Mon, 11 Jan 2016 12:06:55 +0100 Subject: [PATCH 012/649] Adds romansh locale --- arrow/locales.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index c1141674a..36b342f74 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1700,4 +1700,46 @@ def _ordinal_number(self, n): return '{0}ষ্ঠ'.format(n) +class RomanshLocale(locales.Locale): + + names = ['rm', 'rm_ch'] + + past = 'avant {0}' + future = 'en {0}' + + timeframes = { + 'now': 'en quest mument', + 'seconds': 'secundas', + 'minute': 'ina minuta', + 'minutes': '{0} minutas', + 'hour': 'in\'ura', + 'hours': '{0} ura', + 'day': 'in di', + 'days': '{0} dis', + 'month': 'in mais', + 'months': '{0} mais', + 'year': 'in onn', + 'years': '{0} onns', + } + + month_names = [ + '', 'schaner', 'favrer', 'mars', 'avrigl', 'matg', 'zercladur', + 'fanadur', 'avust', 'settember', 'october', 'november', 'december' + ] + + month_abbreviations = [ + '', 'schan', 'fav', 'mars', 'avr', 'matg', 'zer', 'fan', 'avu', + 'set', 'oct', 'nov', 'dec' + ] + + day_names = [ + '', 'glindesdi', 'mardi', 'mesemna', 'gievgia', 'venderdi', + 'sonda', 'dumengia' + ] + + day_abbreviations = [ + '', 'gli', 'ma', 'me', 'gie', 've', 'so', 'du' + ] + + _locales = _map_locales() From b2a3d8388125d3edd47b151374aee600764edf2f Mon Sep 17 00:00:00 2001 From: Baptiste Darthenay Date: Wed, 3 Feb 2016 23:11:07 +0100 Subject: [PATCH 013/649] Adding Esperanto (eo) locale. --- HISTORY.md | 5 +++++ arrow/locales.py | 44 ++++++++++++++++++++++++++++++++++++++++++ tests/locales_tests.py | 14 ++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 0731ee5f4..66fe44799 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ ## History +### 0.7.1 + +- [NEW] Esperanto locale (batisteo) + + ### 0.7.0 - [FIX] Parse localized strings #228 (swistakm) - [FIX] Modify tzinfo parameter in `get` api #221 (bottleimp) diff --git a/arrow/locales.py b/arrow/locales.py index c1141674a..c9e088950 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1587,6 +1587,50 @@ def _format_timeframe(self, timeframe, delta): return form.format(abs(delta)) +class EsperantoLocale(Locale): + names = ['eo', 'eo_xx'] + past = 'antaŭ {0}' + future = 'post {0}' + + timeframes = { + 'now': 'nun', + 'seconds': 'kelkaj sekundoj', + 'minute': 'unu minuto', + 'minutes': '{0} minutoj', + 'hour': 'un horo', + 'hours': '{0} horoj', + 'day': 'unu tago', + 'days': '{0} tagoj', + 'month': 'unu monato', + 'months': '{0} monatoj', + 'year': 'unu jaro', + 'years': '{0} jaroj', + } + + month_names = ['', 'januaro', 'februaro', 'marto', 'aprilo', 'majo', + 'junio', 'julio', 'aŭgusto', 'septembro', 'oktobro', + 'novembro', 'decembro'] + month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'maj', 'jun', + 'jul', 'aŭg', 'sep', 'okt', 'nov', 'dec'] + + day_names = ['', 'lundo', 'mardo', 'merkredo', 'ĵaŭdo', 'vendredo', + 'sabato', 'dimanĉo'] + day_abbreviations = ['', 'lun', 'mar', 'mer', 'ĵaŭ', 'ven', + 'sab', 'dim'] + + meridians = { + 'am': 'atm', + 'pm': 'ptm', + 'AM': 'ATM', + 'PM': 'PTM', + } + + ordinal_day_re = r'((?P[1-3]?[0-9](?=a))a)' + + def _ordinal_number(self, n): + return '{0}a'.format(n) + + class ThaiLocale(Locale): names = ['th', 'th_th'] diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 95a7337d4..34fa907a7 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -375,6 +375,20 @@ def test_format_timeframe(self): assertEqual(self.locale._format_timeframe('now', 0), 'éppen most') +class EsperantoLocaleTests(Chai): + + def setUp(self): + super(EsperantoLocaleTests, self).setUp() + + self.locale = locales.EsperantoLocale() + + def test_format_timeframe(self): + assertEqual(self.locale._format_timeframe('hours', 2), '2 horoj') + assertEqual(self.locale._format_timeframe('hour', 0), 'antaŭ horo') + assertEqual(self.locale._format_timeframe('hours', -2), 'antaŭ 2 horoj') + assertEqual(self.locale._format_timeframe('now', 0), 'nun') + + class ThaiLocaleTests(Chai): def setUp(self): From 39f2762e0dfcf7913296a2a572ee505bd44fdaa4 Mon Sep 17 00:00:00 2001 From: Davide Rizzo Date: Fri, 5 Feb 2016 19:13:41 +0100 Subject: [PATCH 014/649] fixed #301: __rsub__ returns timedelta of opposite sign --- arrow/arrow.py | 6 +++++- tests/arrow_tests.py | 11 ++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index d8c63857a..34a79f070 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -688,7 +688,11 @@ def __sub__(self, other): raise TypeError() def __rsub__(self, other): - return self.__sub__(other) + + if isinstance(other, datetime): + return other - self._datetime + + raise TypeError() # comparisons diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index d0d717d3e..bb10ad664 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -293,11 +293,16 @@ def test_sub_other(self): with assertRaises(TypeError): self.arrow.__sub__(object()) - def test_rsub(self): + def test_rsub_datetime(self): - result = self.arrow.__rsub__(timedelta(days=1)) + result = self.arrow.__rsub__(datetime(2012, 12, 21, tzinfo=tz.tzutc())) - assertEqual(result._datetime, datetime(2012, 12, 31, tzinfo=tz.tzutc())) + assertEqual(result, timedelta(days=-11)) + + def test_rsub_other(self): + + with assertRaises(TypeError): + self.arrow.__rsub__(timedelta(days=1)) class ArrowDatetimeInterfaceTests(Chai): From f2f82d5ba6d08159315fcf5dd14145fea5268bac Mon Sep 17 00:00:00 2001 From: Brendan McCollam Date: Wed, 17 Feb 2016 12:20:01 -0600 Subject: [PATCH 015/649] Adds support for tz offsets without minutes --- arrow/parser.py | 6 ++++-- tests/parser_tests.py | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 2c204aeeb..290a85b64 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -24,7 +24,7 @@ class DateTimeParser(object): _ONE_OR_TWO_DIGIT_RE = re.compile('\d{1,2}') _FOUR_DIGIT_RE = re.compile('\d{4}') _TWO_DIGIT_RE = re.compile('\d{2}') - _TZ_RE = re.compile('[+\-]?\d{2}:?\d{2}') + _TZ_RE = re.compile('[+\-]?\d{2}:?(\d{2})?') _TZ_NAME_RE = re.compile('\w[\w+\-/]+') @@ -273,7 +273,7 @@ def _choice_re(choices, flags=0): class TzinfoParser(object): - _TZINFO_RE = re.compile('([+\-])?(\d\d):?(\d\d)') + _TZINFO_RE = re.compile('([+\-])?(\d\d):?(\d\d)?') @classmethod def parse(cls, string): @@ -292,6 +292,8 @@ def parse(cls, string): if iso_match: sign, hours, minutes = iso_match.groups() + if minutes is None: + minutes = 0 seconds = int(hours) * 3600 + int(minutes) * 60 if sign == '-': diff --git a/tests/parser_tests.py b/tests/parser_tests.py index ed34863e2..b6c4ba367 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -127,6 +127,11 @@ def test_parse_pm(self): assertEqual(self.parser.parse('12 pm', 'H A'), expected) assertEqual(self.parser.parse('12 pm', 'h A'), expected) + def test_parse_tz_hours_only(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') + assertEqual(parsed, expected) + def test_parse_tz_zz(self): expected = datetime(2013, 1, 1, tzinfo=tz.tzoffset(None, -7 * 3600)) From bdf4d86c0808483cbb309ef0623b46d03ba790b0 Mon Sep 17 00:00:00 2001 From: Brendan McCollam Date: Wed, 17 Feb 2016 12:20:27 -0600 Subject: [PATCH 016/649] More conservative claims in documentation --- README.rst | 3 ++- docs/index.rst | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 0129a10f8..cc0e44fdd 100644 --- a/README.rst +++ b/README.rst @@ -42,7 +42,8 @@ Features - Time zone-aware & UTC by default - Provides super-simple creation options for many common input scenarios - Updated .replace method with support for relative offsets, including weeks -- Formats and parses strings, including ISO-8601-formatted strings automatically +- Formats and parses strings automatically +- Partial support for ISO-8601 - Timezone conversion - Timestamp available as a property - Generates time spans, ranges, floors and ceilings in timeframes from year to microsecond diff --git a/docs/index.rst b/docs/index.rst index 6e259c069..ce0a690e5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,7 +30,8 @@ Features - Time zone-aware & UTC by default - Provides super-simple creation options for many common input scenarios - Updated .replace method with support for relative offsets, including weeks -- Formats and parses strings, including ISO-8601-formatted strings automatically +- Formats and parses strings automatically +- Partial ISO-8601 support - Timezone conversion - Timestamp available as a property - Generates time spans, ranges, floors and ceilings in time frames from year to microsecond @@ -145,7 +146,7 @@ Search a date in a string: >>> arrow.get('June was born in May 1980', 'MMMM YYYY') -Many ISO-8601 compliant strings are recognized and parsed without a format string: +Some ISO-8601 compliant strings are recognized and parsed without a format string: >>> arrow.get('2013-09-30T15:34:00.000-07:00') From b886b2310756691403b13f418bdbf34985250ddf Mon Sep 17 00:00:00 2001 From: kitsuyui Date: Sun, 6 Mar 2016 20:16:29 +0900 Subject: [PATCH 017/649] Update Japanese locale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "秒" into "数秒" - It will be "秒前" and "秒後" after it has be concatenated. - It doesn't make sense (smattering). --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index 5b9be80bc..b23895a54 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -379,7 +379,7 @@ class JapaneseLocale(Locale): timeframes = { 'now': '現在', - 'seconds': '秒', + 'seconds': '数秒', 'minute': '1分', 'minutes': '{0}分', 'hour': '1時間', From 4f2303ef337c97f75c564194fa6c0fbc94aaf37e Mon Sep 17 00:00:00 2001 From: Neville Park Date: Wed, 9 Mar 2016 10:27:28 -0500 Subject: [PATCH 018/649] sorry, it was missing Canadian English. --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index 5b9be80bc..796bd0280 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -186,7 +186,7 @@ def _format_relative(self, humanized, timeframe, delta): class EnglishLocale(Locale): - names = ['en', 'en_us', 'en_gb', 'en_au', 'en_be', 'en_jp', 'en_za'] + names = ['en', 'en_us', 'en_gb', 'en_au', 'en_be', 'en_jp', 'en_za', 'en_ca'] past = '{0} ago' future = 'in {0}' From 9650d7d193d66ff834cbedae6edc86abc3f81005 Mon Sep 17 00:00:00 2001 From: Andrew Elkins Date: Wed, 23 Mar 2016 09:35:06 -0700 Subject: [PATCH 019/649] Add locales coverage --- tests/locales_tests.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 0396205ff..041e46293 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -270,6 +270,32 @@ def test_format_relative_past(self): assertEqual(result, 'Před hodinou') +class BulgarianLocaleTests(Chai): + + def test_plurals2(self): + + locale = locales.RussianLocale() + + assertEqual(locale._format_timeframe('hours', 0), '0 часа') + assertEqual(locale._format_timeframe('hours', 1), '1 час') + assertEqual(locale._format_timeframe('hours', 2), '2 часа') + assertEqual(locale._format_timeframe('hours', 4), '4 часа') + assertEqual(locale._format_timeframe('hours', 5), '5 часа') + assertEqual(locale._format_timeframe('hours', 21), '21 час') + assertEqual(locale._format_timeframe('hours', 22), '22 часа') + assertEqual(locale._format_timeframe('hours', 25), '25 часа') + + # feminine grammatical gender should be tested separately + assertEqual(locale._format_timeframe('minutes', 0), '0 минути') + assertEqual(locale._format_timeframe('minutes', 1), '1 минута') + assertEqual(locale._format_timeframe('minutes', 2), '2 минути') + assertEqual(locale._format_timeframe('minutes', 4), '4 минути') + assertEqual(locale._format_timeframe('minutes', 5), '5 минути') + assertEqual(locale._format_timeframe('minutes', 21), '21 минута') + assertEqual(locale._format_timeframe('minutes', 22), '22 минути') + assertEqual(locale._format_timeframe('minutes', 25), '25 минути') + + class HebrewLocaleTests(Chai): def test_couple_of_timeframe(self): From 4e5d3456db1ad112d8b4062e4cc9964970289267 Mon Sep 17 00:00:00 2001 From: Andrew Elkins Date: Wed, 23 Mar 2016 09:38:12 -0700 Subject: [PATCH 020/649] Update version to 0.8.0 --- arrow/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/__init__.py b/arrow/__init__.py index 8407d9966..916297a40 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -4,5 +4,5 @@ from .factory import ArrowFactory from .api import get, now, utcnow -__version__ = '0.7.0' +__version__ = '0.8.0' VERSION = __version__ From adce66bbeb3833112e8dd0fb14caa010cac57af5 Mon Sep 17 00:00:00 2001 From: Dillon Dixon Date: Wed, 23 Mar 2016 13:27:13 -0700 Subject: [PATCH 021/649] Fixed problems with coverage, broken build, and testing --- arrow/locales.py | 2 +- tests/arrow_tests.py | 7 ++----- tests/locales_tests.py | 8 +++++--- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index acb68e7b9..04db24e0a 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1744,7 +1744,7 @@ def _ordinal_number(self, n): return '{0}ষ্ঠ'.format(n) -class RomanshLocale(locales.Locale): +class RomanshLocale(Locale): names = ['rm', 'rm_ch'] diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 80424850b..7735b20e6 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1006,11 +1006,8 @@ def test_month(self): def test_months(self): - later = self.now.replace(months=1) - later = later.replace(days=15) - - earlier = self.now.replace(months=-1) - earlier = earlier.replace(days=-15) + later = self.now.replace(months=2) + earlier = self.now.replace(months=-2) assertEqual(earlier.humanize(self.now), '2 months ago') assertEqual(later.humanize(self.now), 'in 2 months') diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 041e46293..942c3c58a 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -274,7 +274,7 @@ class BulgarianLocaleTests(Chai): def test_plurals2(self): - locale = locales.RussianLocale() + locale = locales.BulgarianLocale() assertEqual(locale._format_timeframe('hours', 0), '0 часа') assertEqual(locale._format_timeframe('hours', 1), '1 час') @@ -410,10 +410,12 @@ def setUp(self): def test_format_timeframe(self): assertEqual(self.locale._format_timeframe('hours', 2), '2 horoj') - assertEqual(self.locale._format_timeframe('hour', 0), 'antaŭ horo') - assertEqual(self.locale._format_timeframe('hours', -2), 'antaŭ 2 horoj') + assertEqual(self.locale._format_timeframe('hour', 0), 'un horo') + assertEqual(self.locale._format_timeframe('hours', -2), '2 horoj') assertEqual(self.locale._format_timeframe('now', 0), 'nun') + def test_ordinal_number(self): + assertEqual(self.locale.ordinal_number(1), '1a') class ThaiLocaleTests(Chai): From fcaa831f1d6036f0a29446755eae243c9b0fbd65 Mon Sep 17 00:00:00 2001 From: Dillon Dixon Date: Wed, 23 Mar 2016 13:34:58 -0700 Subject: [PATCH 022/649] Fixed formatting for Python 2.6 compatibility --- arrow/arrow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index fc787e267..0d2e6f399 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -411,7 +411,7 @@ def replace(self, **kwargs): elif key in self._ATTRS_PLURAL or key in ['weeks', 'quarters']: relative_kwargs[key] = value elif key in ['week', 'quarter']: - raise AttributeError('setting absolute {} is not supported'.format(key)) + raise AttributeError('setting absolute {0} is not supported'.format(key)) elif key !='tzinfo': raise AttributeError() From 945258bd398dedf237ae51b09ccde8c7eb5ea27b Mon Sep 17 00:00:00 2001 From: Finwood Date: Tue, 29 Mar 2016 14:53:52 +0200 Subject: [PATCH 023/649] included error message Added error description at AttributeError on invalid `Arrow.replace` key --- arrow/arrow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 0d2e6f399..b17c435cc 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -413,7 +413,7 @@ def replace(self, **kwargs): elif key in ['week', 'quarter']: raise AttributeError('setting absolute {0} is not supported'.format(key)) elif key !='tzinfo': - raise AttributeError() + raise AttributeError('unknown attribute: "{0}"'.format(key)) # core datetime does not support quarters, translate to months. if 'quarters' in relative_kwargs.keys(): From cd907a06c7b5804551b208c8d3169b81d34fbffb Mon Sep 17 00:00:00 2001 From: Dimitri Merejkowsky Date: Sat, 9 Apr 2016 04:56:46 +0200 Subject: [PATCH 024/649] arrow.get(): Implement d, ddd and ddd Fixes #281 and #273 --- arrow/parser.py | 6 +++++- tests/factory_tests.py | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/arrow/parser.py b/arrow/parser.py index 1e3f6fbd9..6e94a10c4 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -14,7 +14,7 @@ class ParserError(RuntimeError): class DateTimeParser(object): - _FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X)') + _FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X)') _ESCAPE_RE = re.compile('\[[^\[\]]*\]') _ONE_THROUGH_SIX_DIGIT_RE = re.compile('\d{1,6}') @@ -67,6 +67,10 @@ def __init__(self, locale='en_us'): 'MMM': self._choice_re(self.locale.month_abbreviations[1:], re.IGNORECASE), 'Do': re.compile(self.locale.ordinal_day_re), + 'dddd': self._choice_re(self.locale.day_names[1:], re.IGNORECASE), + 'ddd': self._choice_re(self.locale.day_abbreviations[1:], + re.IGNORECASE), + 'd' : re.compile("[1-7]"), 'a': self._choice_re( (self.locale.meridians['am'], self.locale.meridians['pm']) ), diff --git a/tests/factory_tests.py b/tests/factory_tests.py index a669e6cfd..7de47d6d5 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -141,12 +141,21 @@ def test_two_args_str_str(self): assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + 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') + + assertEqual(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']) assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + def test_two_args_unicode_unicode(self): result = self.factory.get(u'2013-01-01', u'YYYY-MM-DD') From 474dfe2ce3cc249b42c140f19de4c0ce0519d32a Mon Sep 17 00:00:00 2001 From: Rishi Ranjan Jha Date: Tue, 26 Apr 2016 12:13:27 +0530 Subject: [PATCH 025/649] Update locales.py --- arrow/locales.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 04db24e0a..58c421bc3 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1205,11 +1205,11 @@ class HindiLocale(Locale): future = '{0} बाद' timeframes = { - 'now': 'अभि', + 'now': 'अभी', 'seconds': 'सेकंड्', 'minute': 'एक मिनट ', 'minutes': '{0} मिनट ', - 'hour': 'एक घंट', + 'hour': 'एक घंटा', 'hours': '{0} घंटे', 'day': 'एक दिन', 'days': '{0} दिन', From 70cc03149b4c4d812d719538a8bda23512059941 Mon Sep 17 00:00:00 2001 From: Rishi Ranjan Jha Date: Tue, 26 Apr 2016 12:55:20 +0530 Subject: [PATCH 026/649] Update locales_tests.py Updated tests for change in hindi locale --- tests/locales_tests.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 942c3c58a..f5ef44914 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -223,19 +223,19 @@ def test_format_timeframe(self): def test_format_relative_now(self): - result = self.locale._format_relative('अभि', 'now', 0) + result = self.locale._format_relative('अभी', 'now', 0) - assertEqual(result, 'अभि') + assertEqual(result, 'अभी') def test_format_relative_past(self): - result = self.locale._format_relative('एक घंट', 'hour', 1) - assertEqual(result, 'एक घंट बाद') + result = self.locale._format_relative('एक घंटा', 'hour', 1) + assertEqual(result, 'एक घंटा बाद') def test_format_relative_future(self): - result = self.locale._format_relative('एक घंट', 'hour', -1) - assertEqual(result, 'एक घंट पहले') + result = self.locale._format_relative('एक घंटा', 'hour', -1) + assertEqual(result, 'एक घंटा पहले') class CzechLocaleTests(Chai): From ab057f70f5c761fca736d99cc6d1ecbaee232f52 Mon Sep 17 00:00:00 2001 From: Rishi Ranjan Jha Date: Tue, 26 Apr 2016 13:00:33 +0530 Subject: [PATCH 027/649] Update locales_tests.py --- tests/locales_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/locales_tests.py b/tests/locales_tests.py index f5ef44914..c4888fa25 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -219,7 +219,7 @@ def setUp(self): def test_format_timeframe(self): assertEqual(self.locale._format_timeframe('hours', 2), '2 घंटे') - assertEqual(self.locale._format_timeframe('hour', 0), 'एक घंट') + assertEqual(self.locale._format_timeframe('hour', 0), 'एक घंटा') def test_format_relative_now(self): From dddb0b0503e44721e1319e04a1cee665502cf8c3 Mon Sep 17 00:00:00 2001 From: youngminz Date: Thu, 5 May 2016 07:52:45 +0900 Subject: [PATCH 028/649] Update Korean Locale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "몇초" to "몇 초": Rules of Korean spelling (Section 43) - "일 분" to "1분": Keep consistency --- arrow/locales.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 58c421bc3..1857cc236 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -559,8 +559,8 @@ class KoreanLocale(Locale): timeframes = { 'now': '지금', - 'seconds': '몇초', - 'minute': '일 분', + 'seconds': '몇 초', + 'minute': '1분', 'minutes': '{0}분', 'hour': '1시간', 'hours': '{0}시간', From e6c1931dc8b27528de35e5dac701ef11d00cfeff Mon Sep 17 00:00:00 2001 From: Alexander Raginsky Date: Fri, 6 May 2016 10:32:20 +0200 Subject: [PATCH 029/649] Fixed ordinal pattern for Italian and Spanish --- arrow/locales.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 58c421bc3..80fe7e328 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -263,10 +263,10 @@ class ItalianLocale(Locale): day_names = ['', 'lunedì', 'martedì', 'mercoledì', 'giovedì', 'venerdì', 'sabato', 'domenica'] day_abbreviations = ['', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab', 'dom'] - ordinal_day_re = r'((?P[1-3]?[0-9](?=°))°)' + ordinal_day_re = r'((?P[1-3]?[0-9](?=[ºª]))[ºª])' def _ordinal_number(self, n): - return '{0}°'.format(n) + return '{0}º'.format(n) class SpanishLocale(Locale): @@ -297,10 +297,10 @@ class SpanishLocale(Locale): day_names = ['', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado', 'domingo'] day_abbreviations = ['', 'lun', 'mar', 'mie', 'jue', 'vie', 'sab', 'dom'] - ordinal_day_re = r'((?P[1-3]?[0-9](?=°))°)' + ordinal_day_re = r'((?P[1-3]?[0-9](?=[ºª]))[ºª])' def _ordinal_number(self, n): - return '{0}°'.format(n) + return '{0}º'.format(n) class FrenchLocale(Locale): From 4749990555e5e544bb8face044b286b82a9785ea Mon Sep 17 00:00:00 2001 From: Alexander Raginsky Date: Fri, 6 May 2016 10:37:53 +0200 Subject: [PATCH 030/649] Fixed locales tests for Spanish & Italian --- tests/locales_tests.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/locales_tests.py b/tests/locales_tests.py index c4888fa25..0a2c85d5b 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -101,7 +101,8 @@ class ItalianLocalesTests(Chai): def test_ordinal_number(self): locale = locales.ItalianLocale() - assertEqual(locale.ordinal_number(1), '1°') + assertEqual(locale.ordinal_number(1), '1º') + assertEqual(locale.ordinal_number(1), '2ª') class SpanishLocalesTests(Chai): @@ -109,7 +110,8 @@ class SpanishLocalesTests(Chai): def test_ordinal_number(self): locale = locales.SpanishLocale() - assertEqual(locale.ordinal_number(1), '1°') + assertEqual(locale.ordinal_number(1), '1º') + assertEqual(locale.ordinal_number(1), '2ª') class FrenchLocalesTests(Chai): From 0b2f44aee05f30d32a184821314a3f2db79a0c86 Mon Sep 17 00:00:00 2001 From: Alexander Raginsky Date: Fri, 6 May 2016 10:52:00 +0200 Subject: [PATCH 031/649] Updating tests --- tests/locales_tests.py | 4 ++-- tests/parser_tests.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 0a2c85d5b..fcab64f6d 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -102,7 +102,7 @@ def test_ordinal_number(self): locale = locales.ItalianLocale() assertEqual(locale.ordinal_number(1), '1º') - assertEqual(locale.ordinal_number(1), '2ª') + assertEqual(locale.ordinal_number(2), '2ª') class SpanishLocalesTests(Chai): @@ -111,7 +111,7 @@ def test_ordinal_number(self): locale = locales.SpanishLocale() assertEqual(locale.ordinal_number(1), '1º') - assertEqual(locale.ordinal_number(1), '2ª') + assertEqual(locale.ordinal_number(2), '2ª') class FrenchLocalesTests(Chai): diff --git a/tests/parser_tests.py b/tests/parser_tests.py index e2bb89284..59a84dbc9 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -646,13 +646,13 @@ def test_english(self): def test_italian(self): parser_ = parser.DateTimeParser('it_it') - assertEqual(parser_.parse('Gennaio 1°, 2013', 'MMMM Do, YYYY'), + assertEqual(parser_.parse('Gennaio 1º, 2013', 'MMMM Do, YYYY'), datetime(2013, 1, 1)) def test_spanish(self): parser_ = parser.DateTimeParser('es_es') - assertEqual(parser_.parse('Enero 1°, 2013', 'MMMM Do, YYYY'), + assertEqual(parser_.parse('Enero 1º, 2013', 'MMMM Do, YYYY'), datetime(2013, 1, 1)) def test_french(self): From 2b3fac2dcbd85d9a613163ad6e91e52b4d51c7da Mon Sep 17 00:00:00 2001 From: Alexander Raginsky Date: Fri, 6 May 2016 10:56:11 +0200 Subject: [PATCH 032/649] Updating tests --- tests/locales_tests.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/locales_tests.py b/tests/locales_tests.py index fcab64f6d..28bffae1c 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -102,7 +102,6 @@ def test_ordinal_number(self): locale = locales.ItalianLocale() assertEqual(locale.ordinal_number(1), '1º') - assertEqual(locale.ordinal_number(2), '2ª') class SpanishLocalesTests(Chai): @@ -111,7 +110,6 @@ def test_ordinal_number(self): locale = locales.SpanishLocale() assertEqual(locale.ordinal_number(1), '1º') - assertEqual(locale.ordinal_number(2), '2ª') class FrenchLocalesTests(Chai): From 77bf4769419eaf744f73e00bb79fe2b7a0567420 Mon Sep 17 00:00:00 2001 From: Rishi Ranjan Jha Date: Thu, 12 May 2016 11:38:07 +0530 Subject: [PATCH 033/649] Update locales.py --- arrow/locales.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 58c421bc3..0add542b2 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1226,8 +1226,8 @@ class HindiLocale(Locale): 'PM': 'शाम', } - month_names = ['', 'जनवरी', 'फ़रवरी', 'मार्च', 'अप्रैल ', 'मई', 'जून', 'जुलाई', - 'आगस्त', 'सितम्बर', 'अकतूबर', 'नवेम्बर', 'दिसम्बर'] + month_names = ['', 'जनवरी', 'फरवरी', 'मार्च', 'अप्रैल ', 'मई', 'जून', 'जुलाई', + 'अगस्त', 'सितंबर', 'अक्टूबर', 'नवंबर', 'दिसंबर'] month_abbreviations = ['', 'जन', 'फ़र', 'मार्च', 'अप्रै', 'मई', 'जून', 'जुलाई', 'आग', 'सित', 'अकत', 'नवे', 'दिस'] From f35836eb13f226b36fa85202be7c1968ac1a3092 Mon Sep 17 00:00:00 2001 From: Youngmin Koo Date: Mon, 16 May 2016 21:17:28 +0900 Subject: [PATCH 034/649] Format ParserError's raise message --- arrow/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/parser.py b/arrow/parser.py index 6e94a10c4..f56dcdd8c 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -326,6 +326,6 @@ def parse(cls, string): tzinfo = tz.gettz(string) if tzinfo is None: - raise ParserError('Could not parse timezone expression "{0}"', string) + raise ParserError('Could not parse timezone expression "{0}"'.format(string)) return tzinfo From d7120026a297dc8300d18675aa2070e288930057 Mon Sep 17 00:00:00 2001 From: sipp11 Date: Tue, 17 May 2016 23:55:36 +0700 Subject: [PATCH 035/649] Restore old replace-shift & add deprecated warning Replace w/shift function will be removed in next release hopefully --- arrow/arrow.py | 16 ++++++++++++++++ tests/arrow_tests.py | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/arrow/arrow.py b/arrow/arrow.py index 7a5c3bfe2..5bec032cb 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -12,6 +12,8 @@ from dateutil.relativedelta import relativedelta import calendar import sys +import warnings + from arrow import util, locales, parser, formatter @@ -383,6 +385,12 @@ def replace(self, **kwargs): >>> arw.replace(tzinfo=tz.tzlocal()) + NOTE: Deprecated in next release + Use plural property names to shift their current value relatively: + + >>> arw.replace(years=1, months=-1) + + Recognized timezone expressions: - A ``tzinfo`` object. @@ -393,17 +401,25 @@ def replace(self, **kwargs): ''' absolute_kwargs = {} + relative_kwargs = {} # TODO: DEPRECATED; remove in next release for key, value in kwargs.items(): if key in self._ATTRS: absolute_kwargs[key] = value + elif key in self._ATTRS_PLURAL or key == 'weeks': + # TODO: DEPRECATED + warnings.warn("replace() with plural property to shift value" + "is deprecated, use shift() instead", + DeprecationWarning) + relative_kwargs[key] = value elif key == 'week': raise AttributeError('setting absolute week is not supported') elif key !='tzinfo': raise AttributeError() current = self._datetime.replace(**absolute_kwargs) + current += relativedelta(**relative_kwargs) # TODO: DEPRECATED tzinfo = kwargs.get('tzinfo') diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 16bde5ab9..e14208dc7 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -7,6 +7,7 @@ from datetime import date, datetime, timedelta from dateutil import tz import simplejson as json +import warnings import calendar import pickle import time @@ -203,6 +204,28 @@ def test_ne(self): assertFalse(self.arrow != self.arrow.datetime) assertTrue(self.arrow != 'abc') + def test_deprecated_replace(self): + + with warnings.catch_warnings(record=True) as w: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + # Trigger a warning. + self.arrow.replace(weeks=1) + # Verify some things + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "deprecated" in str(w[-1].message) + + with warnings.catch_warnings(record=True) as w: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + # Trigger a warning. + self.arrow.replace(hours=1) + # Verify some things + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "deprecated" in str(w[-1].message) + def test_gt(self): arrow_cmp = self.arrow.shift(minutes=1) From cf4888c21de0b41a90295e4b9471618e817d159a Mon Sep 17 00:00:00 2001 From: Emin Mastizada Date: Wed, 25 May 2016 21:18:58 +0300 Subject: [PATCH 036/649] Azerbaijani locale added, locale issue fixed in Turkish. --- arrow/locales.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index 5af426746..d03d36aa6 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1034,7 +1034,7 @@ class TurkishLocale(Locale): 'days': '{0} gün', 'month': 'bir ay', 'months': '{0} ay', - 'year': 'a yıl', + 'year': 'yıl', 'years': '{0} yıl', } @@ -1047,6 +1047,37 @@ class TurkishLocale(Locale): day_abbreviations = ['', 'Pzt', 'Sal', 'Çar', 'Per', 'Cum', 'Cmt', 'Paz'] +class AzerbaijaniLocale(Locale): + + names = ['az', 'az_az'] + + past = '{0} əvvəl' + future = '{0} sonra' + + timeframes = { + 'now': 'indi', + 'seconds': 'saniyə', + 'minute': 'bir dəqiqə', + 'minutes': '{0} dəqiqə', + 'hour': 'bir saat', + 'hours': '{0} saat', + 'day': 'bir gün', + 'days': '{0} gün', + 'month': 'bir ay', + 'months': '{0} ay', + 'year': 'il', + 'years': '{0} il', + } + + month_names = ['', 'Yanvar', 'Fevral', 'Mart', 'Aprel', 'May', 'İyun', 'İyul', + 'Avqust', 'Sentyabr', 'Oktyabr', 'Noyabr', 'Dekabr'] + month_abbreviations = ['', 'Yan', 'Fev', 'Mar', 'Apr', 'May', 'İyn', 'İyl', 'Avq', + 'Sen', 'Okt', 'Noy', 'Dek'] + + day_names = ['', 'Bazar ertəsi', 'Çərşənbə axşamı', 'Çərşənbə', 'Cümə axşamı', 'Cümə', 'Şənbə', 'Bazar'] + day_abbreviations = ['', 'Ber', 'Çax', 'Çər', 'Cax', 'Cüm', 'Şnb', 'Bzr'] + + class ArabicLocale(Locale): names = ['ar', 'ar_eg'] From f3982bc7e169142e616f8a99fb8dcd945ae2afc8 Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Fri, 17 Jun 2016 10:49:27 +1000 Subject: [PATCH 037/649] Update supported Python versions in README Include 3.4 and 3.5 in supported versions. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index cc0e44fdd..2c3ffef5e 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ Features -------- - Fully implemented, drop-in replacement for datetime -- Supports Python 2.6, 2.7 and 3.3 +- Supports Python 2.6, 2.7, 3.3, 3.4 and 3.5 - Time zone-aware & UTC by default - Provides super-simple creation options for many common input scenarios - Updated .replace method with support for relative offsets, including weeks From 82aea14cf0abf4725726215d1e594cbbdbb500be Mon Sep 17 00:00:00 2001 From: Caleb Hattingh Date: Fri, 17 Jun 2016 10:50:53 +1000 Subject: [PATCH 038/649] Updated support Python versions also in docs. --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index e58e12048..a4d2475bb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,7 +26,7 @@ Features -------- - Fully implemented, drop-in replacement for datetime -- Supports Python 2.6, 2.7 and 3.3 +- Supports Python 2.6, 2.7, 3.3, 3.4 and 3.5 - Time zone-aware & UTC by default - Provides super-simple creation options for many common input scenarios - Updated .replace method with support for relative offsets, including weeks From 91682cac36296edb6703fc28bec2b93e6fac1080 Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Thu, 23 Jun 2016 23:33:31 -0400 Subject: [PATCH 039/649] Docs: clarifying where the timezone is replaced vs converted, and other tweaks --- arrow/api.py | 12 ++--- arrow/arrow.py | 130 ++++++++++++++++++++++++++--------------------- arrow/factory.py | 34 ++++++------- arrow/parser.py | 1 + docs/index.rst | 27 ++++++---- 5 files changed, 111 insertions(+), 93 deletions(-) diff --git a/arrow/api.py b/arrow/api.py index 495eef490..a624d8c0a 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -13,18 +13,17 @@ # internal default factory. _factory = ArrowFactory() +# FIXME Put the docs here, not in the factory def get(*args, **kwargs): - ''' Implements the default :class:`ArrowFactory ` - ``get`` method. + ''' Calls the default :class:`ArrowFactory ` ``get`` method. ''' return _factory.get(*args, **kwargs) def utcnow(): - ''' Implements the default :class:`ArrowFactory ` - ``utcnow`` method. + ''' Calls the default :class:`ArrowFactory ` ``utcnow`` method. ''' @@ -32,8 +31,7 @@ def utcnow(): def now(tz=None): - ''' Implements the default :class:`ArrowFactory ` - ``now`` method. + ''' Calls the default :class:`ArrowFactory ` ``now`` method. ''' @@ -51,5 +49,5 @@ def factory(type): return ArrowFactory(type) +# FIXME iso? __all__ = ['get', 'utcnow', 'now', 'factory', 'iso'] - diff --git a/arrow/arrow.py b/arrow/arrow.py index b17c435cc..8b56432db 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -29,9 +29,16 @@ class Arrow(object): :param minute: (optional) the minute, Defaults to 0. :param second: (optional) the second, Defaults to 0. :param microsecond: (optional) the microsecond. Defaults 0. - :param tzinfo: (optional) the ``tzinfo`` object. Defaults to ``None``. + :param tzinfo: (optional) A timezone expression. Defaults to UTC. - If tzinfo is None, it is assumed to be UTC on creation. + .. _tz-expr: + + Recognized timezone expressions: + + - A ``tzinfo`` object. + - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. + - A ``str`` in ISO-8601 style, as in '+07:00'. + - A ``str``, one of the following: 'local', 'utc', 'UTC'. Usage:: @@ -40,6 +47,7 @@ class Arrow(object): ''' + # FIXME Why not accept the same set of timezone expressions everywhere? resolution = datetime.resolution @@ -62,7 +70,8 @@ def __init__(self, year, month, day, hour=0, minute=0, second=0, microsecond=0, @classmethod def now(cls, tzinfo=None): - '''Constructs an :class:`Arrow ` object, representing "now". + '''Constructs an :class:`Arrow ` object, representing "now" in the given + timezone. :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. @@ -88,11 +97,17 @@ def utcnow(cls): @classmethod def fromtimestamp(cls, timestamp, tzinfo=None): - ''' Constructs an :class:`Arrow ` object from a timestamp. + ''' Constructs an :class:`Arrow ` object from a timestamp, converted to + the given timezone. :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. + Timestamps should always be UTC. If you have a non-UTC timestamp: + + >>> arrow.Arrow.utcfromtimestamp(1367900664) .replace(tzinfo='US/Pacific') + + ''' tzinfo = tzinfo or dateutil_tz.tzlocal() @@ -118,11 +133,18 @@ def utcfromtimestamp(cls, timestamp): @classmethod def fromdatetime(cls, dt, tzinfo=None): - ''' Constructs an :class:`Arrow ` object from a ``datetime`` and optional - ``tzinfo`` object. + ''' Constructs an :class:`Arrow ` object from a ``datetime`` and + optional replacement ``tzinfo`` object. :param dt: the ``datetime`` - :param tzinfo: (optional) a ``tzinfo`` object. Defaults to UTC. + :param tzinfo: (optional) a ``tzinfo`` object. Defaults to dt.tzinfo, or UTC if naive. + + If you only want to replace the timezone of naive datetimes: + + >>> dt + datetime.datetime(2013, 5, 5, 0, 0, tzinfo=tzutc()) + >>> arrow.Arrow.fromdatetime(dt, dt.tzinfo or 'US/Pacific') + ''' @@ -134,7 +156,7 @@ def fromdatetime(cls, dt, tzinfo=None): @classmethod def fromdate(cls, date, tzinfo=None): ''' Constructs an :class:`Arrow ` object from a ``date`` and optional - ``tzinfo`` object. Time values are set to 0. + replacement ``tzinfo`` object. Time values are set to 0. :param date: the ``date`` :param tzinfo: (optional) a ``tzinfo`` object. Defaults to UTC. @@ -147,15 +169,19 @@ def fromdate(cls, date, tzinfo=None): @classmethod def strptime(cls, date_str, fmt, tzinfo=None): ''' Constructs an :class:`Arrow ` object from a date string and format, - in the style of ``datetime.strptime``. + in the style of ``datetime.strptime``. Optionally replaces the parsed timezone. :param date_str: the date string. :param fmt: the format string. - :param tzinfo: (optional) an optional ``tzinfo`` + :param tzinfo: (optional) a ``tzinfo`` object. Defaults to the parsed timezone if found, + otherwise UTC. + ''' + # FIXME What if there's a %Z but it's missing in date_str? + # FIXME Document how to replace only if not present dt = datetime.strptime(date_str, fmt) - tzinfo = tzinfo or dt.tzinfo + tzinfo = tzinfo or dt.tzinfo or dateutil_tz.tzutc() return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond, tzinfo) @@ -165,33 +191,28 @@ def strptime(cls, date_str, fmt, tzinfo=None): @classmethod def range(cls, frame, start, end=None, tz=None, limit=None): - ''' Returns an array of :class:`Arrow ` objects, representing + ''' Returns a list of :class:`Arrow ` objects, representing an iteration of time between two inputs. :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). :param start: A datetime expression, the start of the range. :param end: (optional) A datetime expression, the end of the range. - :param tz: (optional) A timezone expression. Defaults to UTC. + :param tz: (optional) A :ref:`timezone expression `. Defaults to UTC. :param limit: (optional) A maximum number of tuples to return. - **NOTE**: the **end** or **limit** must be provided. Call with **end** alone to - return the entire range, with **limit** alone to return a maximum # of results from the - start, and with both to cap a range at a maximum # of results. + **NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to + return the entire range. Call with ``limit`` alone to return a maximum # of results from + the start. Call with both to cap a range at a maximum # of results. + + **NOTE**: Unlike Python's ``range``, ``end`` **may** be included in the returned list. - Supported frame values: year, quarter, month, week, day, hour, minute, second + Supported frame values: year, quarter, month, week, day, hour, minute, second. Recognized datetime expressions: - An :class:`Arrow ` object. - A ``datetime`` object. - Recognized timezone expressions: - - - A ``tzinfo`` object. - - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO-8601 style, as in '+07:00'. - - A ``str``, one of the following: 'local', 'utc', 'UTC'. - Usage: >>> start = datetime(2013, 5, 5, 12, 30) @@ -206,6 +227,7 @@ def range(cls, frame, start, end=None, tz=None, limit=None): ''' + # FIXME Role of tz? _, frame_relative, relative_steps = cls._get_frames(frame) @@ -229,33 +251,29 @@ def range(cls, frame, start, end=None, tz=None, limit=None): @classmethod def span_range(cls, frame, start, end, tz=None, limit=None): - ''' Returns an array of tuples, each :class:`Arrow ` objects, + ''' Returns a list of tuples, each :class:`Arrow ` objects, representing a series of timespans between two inputs. :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). :param start: A datetime expression, the start of the range. :param end: (optional) A datetime expression, the end of the range. - :param tz: (optional) A timezone expression. Defaults to UTC. + :param tz: (optional) A :ref:`timezone expression `. Defaults to UTC. :param limit: (optional) A maximum number of tuples to return. - **NOTE**: the **end** or **limit** must be provided. Call with **end** alone to - return the entire range, with **limit** alone to return a maximum # of results from the - start, and with both to cap a range at a maximum # of results. + **NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to + return the entire range. Call with ``limit`` alone to return a maximum # of results from + the start. Call with both to cap a range at a maximum # of results. + + **NOTE**: Unlike Python's ``range``, ``end`` **may** be included in the returned list of + tuples. - Supported frame values: year, quarter, month, week, day, hour, minute, second + Supported frame values: year, quarter, month, week, day, hour, minute, second. Recognized datetime expressions: - An :class:`Arrow ` object. - A ``datetime`` object. - Recognized timezone expressions: - - - A ``tzinfo`` object. - - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO-8601 style, as in '+07:00'. - - A ``str``, one of the following: 'local', 'utc', 'UTC'. - Usage: >>> start = datetime(2013, 5, 5, 12, 30) @@ -270,6 +288,7 @@ def span_range(cls, frame, start, end, tz=None, limit=None): (, ) ''' + # FIXME Role of tz? tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) start = cls.fromdatetime(start, tzinfo).span(frame)[0] _range = cls.range(frame, start, end, tz, limit) @@ -328,6 +347,7 @@ def tzinfo(self): def tzinfo(self, tzinfo): ''' Sets the ``tzinfo`` of the :class:`Arrow ` object. ''' + # FIXME Whoah! Arrows are mutable? I don't think I like that... self._datetime = self._datetime.replace(tzinfo=tzinfo) @property @@ -344,13 +364,15 @@ def naive(self): @property def timestamp(self): - ''' Returns a timestamp representation of the :class:`Arrow ` object. ''' + ''' Returns a timestamp representation of the :class:`Arrow ` object, in + UTC time. ''' return calendar.timegm(self._datetime.utctimetuple()) @property def float_timestamp(self): - ''' Returns a floating-point representation of the :class:`Arrow ` object. ''' + ''' Returns a floating-point representation of the :class:`Arrow ` + object, in UTC time. ''' return self.timestamp + float(self.microsecond) / 1000000 @@ -387,18 +409,12 @@ def replace(self, **kwargs): >>> arw.replace(years=1, months=-1) - You can also provide a timezone expression can also be replaced: + You can also replace the timezone without conversion, using a + :ref:`timezone expression `: >>> arw.replace(tzinfo=tz.tzlocal()) - Recognized timezone expressions: - - - A ``tzinfo`` object. - - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO-8601 style, as in '+07:00'. - - A ``str``, one of the following: 'local', 'utc', 'UTC'. - ''' absolute_kwargs = {} @@ -421,7 +437,7 @@ def replace(self, **kwargs): relative_kwargs['months'] = 0 relative_kwargs['months'] += (value * self._MONTHS_PER_QUARTER) relative_kwargs.pop('quarters') - + current = self._datetime.replace(**absolute_kwargs) current += relativedelta(**relative_kwargs) @@ -437,14 +453,7 @@ def to(self, tz): ''' Returns a new :class:`Arrow ` object, converted to the target timezone. - :param tz: an expression representing a timezone. - - Recognized timezone expressions: - - - A ``tzinfo`` object. - - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO-8601 style, as in '+07:00'. - - A ``str``, one of the following: 'local', 'utc', 'UTC'. + :param tz: A :ref:`timezone expression `. Usage:: @@ -484,7 +493,7 @@ def span(self, frame, count=1): :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). :param count: (optional) the number of frames to span. - Supported frame values: year, quarter, month, week, day, hour, minute, second + Supported frame values: year, quarter, month, week, day, hour, minute, second. Usage:: @@ -772,7 +781,7 @@ def timetz(self): return self._datetime.timetz() def astimezone(self, tz): - ''' Returns a ``datetime`` object, adjusted to the specified tzinfo. + ''' Returns a ``datetime`` object, converted to the specified timezone. :param tz: a ``tzinfo`` object. @@ -781,12 +790,14 @@ def astimezone(self, tz): return self._datetime.astimezone(tz) def utcoffset(self): - ''' Returns a ``timedelta`` object representing the whole number of minutes difference from UTC time. ''' + ''' Returns a ``timedelta`` object representing the whole number of minutes difference from + UTC time. ''' return self._datetime.utcoffset() def dst(self): ''' Returns the daylight savings time adjustment. ''' + return self._datetime.dst() def timetuple(self): @@ -840,6 +851,7 @@ def strftime(self, format): def for_json(self): '''Serializes for the ``for_json`` protocol of simplejson.''' + return self.isoformat() # internal tools. diff --git a/arrow/factory.py b/arrow/factory.py index a5d690b22..8a1fecc6e 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -44,7 +44,7 @@ def get(self, *args, **kwargs): **None** to also get current UTC time:: >>> arrow.get(None) - + **One** :class:`Arrow ` object, to get a copy. @@ -52,7 +52,8 @@ def get(self, *args, **kwargs): >>> arrow.get(arw) - **One** ``str``, ``float``, or ``int``, convertible to a floating-point timestamp, to get that timestamp in UTC:: + **One** ``str``, ``float``, or ``int``, convertible to a floating-point timestamp, to get + that timestamp in UTC:: >>> arrow.get(1367992474.293378) @@ -64,13 +65,15 @@ def get(self, *args, **kwargs): >>> arrow.get('1367992474') - + **One** ISO-8601-formatted ``str``, to parse it:: >>> arrow.get('2013-09-29T01:26:43.830580') + FIXME This is a conversion, not a replacement! One of these should go away. + **One** ``tzinfo``, to get the current time in that timezone:: >>> arrow.get(tz.tzlocal()) @@ -91,12 +94,14 @@ def get(self, *args, **kwargs): >>> arrow.get(date(2013, 5, 5)) - **Two** arguments, a naive or aware ``datetime``, and a timezone expression (as above):: + **Two** arguments, a naive or aware ``datetime``, and a replacement + :ref:`timezone expression `:: >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific') - **Two** arguments, a naive ``date``, and a timezone expression (as above):: + **Two** arguments, a naive ``date``, and a replacement + :ref:`timezone expression `:: >>> arrow.get(date(2013, 5, 5), 'US/Pacific') @@ -117,6 +122,7 @@ def get(self, *args, **kwargs): **One** time.struct time:: + >>> arrow.get(gmtime(0)) @@ -159,7 +165,7 @@ def get(self, *args, **kwargs): elif isinstance(arg, tzinfo): return self.type.now(arg) - # (str) -> now, @ tzinfo. + # (str) -> parse. elif isstr(arg): dt = parser.DateTimeParser(locale).parse_iso(arg) return self.type.fromdatetime(dt) @@ -177,16 +183,16 @@ def get(self, *args, **kwargs): if isinstance(arg_1, datetime): - # (datetime, tzinfo) -> fromdatetime @ tzinfo/string. + # (datetime, tzinfo/str) -> fromdatetime replace tzinfo. if isinstance(arg_2, tzinfo) or isstr(arg_2): return self.type.fromdatetime(arg_1, arg_2) else: raise TypeError('Can\'t parse two arguments of types \'datetime\', \'{0}\''.format( type(arg_2))) - # (date, tzinfo/str) -> fromdate @ tzinfo/string. elif isinstance(arg_1, date): + # (date, tzinfo/str) -> fromdate replace tzinfo. if isinstance(arg_2, tzinfo) or isstr(arg_2): return self.type.fromdate(arg_1, tzinfo=arg_2) else: @@ -219,16 +225,10 @@ def utcnow(self): return self.type.utcnow() def now(self, tz=None): - '''Returns an :class:`Arrow ` object, representing "now". - - :param tz: (optional) An expression representing a timezone. Defaults to local time. - - Recognized timezone expressions: + '''Returns an :class:`Arrow ` object, representing "now" in the given + timezone. - - A ``tzinfo`` object. - - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO-8601 style, as in '+07:00'. - - A ``str``, one of the following: 'local', 'utc', 'UTC'. + :param tz: (optional) A :ref:`timezone expression `. Defaults to local time. Usage:: diff --git a/arrow/parser.py b/arrow/parser.py index 6e94a10c4..1925889c5 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -292,6 +292,7 @@ def _choice_re(choices, flags=0): return re.compile('({0})'.format('|'.join(choices)), flags=flags) +# FIXME Expose as arrow.gettz? class TzinfoParser(object): _TZINFO_RE = re.compile('([+\-])?(\d\d):?(\d\d)?') diff --git a/docs/index.rst b/docs/index.rst index e58e12048..52b979743 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,12 +13,12 @@ Arrow is heavily inspired by `moment.js `_ a ---- Why? ---- -Python's standard library and some other low-level modules have near-complete date, time and time zone functionality but don't work very well from a usability perspective: +Python's standard library and some other low-level modules have near-complete date, time and timezone functionality but don't work very well from a usability perspective: - Too many modules: datetime, time, calendar, dateutil, pytz and more - Too many types: date, time, datetime, tzinfo, timedelta, relativedelta, etc. -- Time zones and timestamp conversions are verbose and unpleasant -- Time zone naivety is the norm +- Timezones and timestamp conversions are verbose and unpleasant +- Timezone naivety is the norm - Gaps in functionality: ISO-8601 parsing, time spans, humanization -------- @@ -26,8 +26,8 @@ Features -------- - Fully implemented, drop-in replacement for datetime -- Supports Python 2.6, 2.7 and 3.3 -- Time zone-aware & UTC by default +- Supports Python 2.6, 2.7 and 3.3+ +- Timezone-aware & UTC by default - Provides super-simple creation options for many common input scenarios - Updated .replace method with support for relative offsets, including weeks - Formats and parses strings automatically @@ -115,19 +115,19 @@ Create from timestamps (ints or floats, or strings that convert to a float): >>> arrow.get('1367900664.152325') -Use a naive or timezone-aware datetime, or flexibly specify a time zone: +Use a naive or timezone-aware datetime, or flexibly specify a timezone: .. code-block:: python >>> arrow.get(datetime.utcnow()) - >>> arrow.get(datetime.now(), 'US/Pacific') - + >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific') + >>> from dateutil import tz - >>> arrow.get(datetime.now(), tz.gettz('US/Pacific')) - + >>> arrow.get(datetime(2013, 5, 5), tz.gettz('US/Pacific')) + >>> arrow.get(datetime.now(tz.gettz('US/Pacific'))) @@ -223,6 +223,13 @@ Or, get one with attributes shifted forward or backward: >>> arw.replace(weeks=+3) +Even replace the timezone without altering other attributes: + +.. code-block:: python + + >>> arw.replace(tzinfo='US/Pacific') + + Format ====== From ae503cc9dee577248060186e64036cceefe4cf81 Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Sun, 26 Jun 2016 14:52:56 -0400 Subject: [PATCH 040/649] Improve (and fix!) `range` and `span_range` documentation, and other doc tweaks --- arrow/api.py | 4 +--- arrow/arrow.py | 58 +++++++++++++++++++++++++++--------------------- arrow/factory.py | 4 +--- arrow/parser.py | 1 - docs/conf.py | 4 ++-- 5 files changed, 37 insertions(+), 34 deletions(-) diff --git a/arrow/api.py b/arrow/api.py index a624d8c0a..81a5c5878 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -13,7 +13,6 @@ # internal default factory. _factory = ArrowFactory() -# FIXME Put the docs here, not in the factory def get(*args, **kwargs): ''' Calls the default :class:`ArrowFactory ` ``get`` method. @@ -49,5 +48,4 @@ def factory(type): return ArrowFactory(type) -# FIXME iso? -__all__ = ['get', 'utcnow', 'now', 'factory', 'iso'] +__all__ = ['get', 'utcnow', 'now', 'factory'] diff --git a/arrow/arrow.py b/arrow/arrow.py index 8b56432db..a02eaa741 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -47,7 +47,6 @@ class Arrow(object): ''' - # FIXME Why not accept the same set of timezone expressions everywhere? resolution = datetime.resolution @@ -103,7 +102,7 @@ def fromtimestamp(cls, timestamp, tzinfo=None): :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. - Timestamps should always be UTC. If you have a non-UTC timestamp: + Timestamps should always be UTC. If you have a non-UTC timestamp:: >>> arrow.Arrow.utcfromtimestamp(1367900664) .replace(tzinfo='US/Pacific') @@ -139,7 +138,7 @@ def fromdatetime(cls, dt, tzinfo=None): :param dt: the ``datetime`` :param tzinfo: (optional) a ``tzinfo`` object. Defaults to dt.tzinfo, or UTC if naive. - If you only want to replace the timezone of naive datetimes: + If you only want to replace the timezone of naive datetimes:: >>> dt datetime.datetime(2013, 5, 5, 0, 0, tzinfo=tzutc()) @@ -204,8 +203,6 @@ def range(cls, frame, start, end=None, tz=None, limit=None): return the entire range. Call with ``limit`` alone to return a maximum # of results from the start. Call with both to cap a range at a maximum # of results. - **NOTE**: Unlike Python's ``range``, ``end`` **may** be included in the returned list. - Supported frame values: year, quarter, month, week, day, hour, minute, second. Recognized datetime expressions: @@ -213,12 +210,12 @@ def range(cls, frame, start, end=None, tz=None, limit=None): - An :class:`Arrow ` object. - A ``datetime`` object. - Usage: + Usage:: >>> 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) + ... print(repr(r)) ... @@ -226,6 +223,16 @@ def range(cls, frame, start, end=None, tz=None, limit=None): + **NOTE**: Unlike Python's ``range``, ``end`` *may* be included in the returned list:: + + >>> start = datetime(2013, 5, 5, 12, 30) + >>> end = datetime(2013, 5, 5, 13, 30) + >>> for r in arrow.Arrow.range('hour', start, end): + ... print(repr(r)) + ... + + + ''' # FIXME Role of tz? @@ -264,9 +271,6 @@ def span_range(cls, frame, start, end, tz=None, limit=None): return the entire range. Call with ``limit`` alone to return a maximum # of results from the start. Call with both to cap a range at a maximum # of results. - **NOTE**: Unlike Python's ``range``, ``end`` **may** be included in the returned list of - tuples. - Supported frame values: year, quarter, month, week, day, hour, minute, second. Recognized datetime expressions: @@ -274,18 +278,22 @@ def span_range(cls, frame, start, end, tz=None, limit=None): - An :class:`Arrow ` object. - A ``datetime`` object. + **NOTE**: Unlike Python's ``range``, ``end`` will *always* be included in the returned list + of timespans. + Usage: >>> 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 + ... print(r) ... (, ) (, ) (, ) (, ) (, ) + (, ) ''' # FIXME Role of tz? @@ -347,7 +355,6 @@ def tzinfo(self): def tzinfo(self, tzinfo): ''' Sets the ``tzinfo`` of the :class:`Arrow ` object. ''' - # FIXME Whoah! Arrows are mutable? I don't think I like that... self._datetime = self._datetime.replace(tzinfo=tzinfo) @property @@ -395,25 +402,25 @@ def replace(self, **kwargs): ''' Returns a new :class:`Arrow ` object with attributes updated according to inputs. - Use single property names to set their value absolutely: + Use single property names to set their value absolutely:: - >>> import arrow - >>> arw = arrow.utcnow() - >>> arw - - >>> arw.replace(year=2014, month=6) - + >>> import arrow + >>> arw = arrow.utcnow() + >>> arw + + >>> arw.replace(year=2014, month=6) + - Use plural property names to shift their current value relatively: + Use plural property names to shift their current value relatively:: - >>> arw.replace(years=1, months=-1) - + >>> arw.replace(years=1, months=-1) + You can also replace the timezone without conversion, using a - :ref:`timezone expression `: + :ref:`timezone expression `:: - >>> arw.replace(tzinfo=tz.tzlocal()) - + >>> arw.replace(tzinfo=tz.tzlocal()) + ''' @@ -607,6 +614,7 @@ def humanize(self, other=None, locale='en_us', only_distance=False): Defaults to now in the current :class:`Arrow ` object's timezone. :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'. :param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part. + Usage:: >>> earlier = arrow.utcnow().replace(hours=-2) diff --git a/arrow/factory.py b/arrow/factory.py index 8a1fecc6e..8fae51fda 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -72,9 +72,7 @@ def get(self, *args, **kwargs): >>> arrow.get('2013-09-29T01:26:43.830580') - FIXME This is a conversion, not a replacement! One of these should go away. - - **One** ``tzinfo``, to get the current time in that timezone:: + **One** ``tzinfo``, to get the current time **converted** to that timezone:: >>> arrow.get(tz.tzlocal()) diff --git a/arrow/parser.py b/arrow/parser.py index 1925889c5..6e94a10c4 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -292,7 +292,6 @@ def _choice_re(choices, flags=0): return re.compile('({0})'.format('|'.join(choices)), flags=flags) -# FIXME Expose as arrow.gettz? class TzinfoParser(object): _TZINFO_RE = re.compile('([+\-])?(\d\d):?(\d\d)?') diff --git a/docs/conf.py b/docs/conf.py index 92fa464ee..bd0bdecec 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -64,7 +64,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ['_build', '_themes/**README.rst'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None @@ -123,7 +123,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. From c8f0873625cc6b4391759deef9a1abd823c35662 Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Sun, 26 Jun 2016 15:26:34 -0400 Subject: [PATCH 041/649] Clarify the role of ``tz`` in ``range`` and ``span_range`` --- arrow/arrow.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index a02eaa741..a138b4078 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -196,13 +196,18 @@ def range(cls, frame, start, end=None, tz=None, limit=None): :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). :param start: A datetime expression, the start of the range. :param end: (optional) A datetime expression, the end of the range. - :param tz: (optional) A :ref:`timezone expression `. Defaults to UTC. + :param tz: (optional) A :ref:`timezone expression `. Defaults to + ``start``'s timezone, or UTC if ``start`` is naive. :param limit: (optional) A maximum number of tuples to return. **NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to return the entire range. Call with ``limit`` alone to return a maximum # of results from the start. Call with both to cap a range at a maximum # of results. + **NOTE**: ``tz`` internally **replaces** the timezones of both ``start`` and ``end`` before + iterating. As such, either call with naive objects and ``tz``, or aware objects from the + same timezone and no ``tz``. + Supported frame values: year, quarter, month, week, day, hour, minute, second. Recognized datetime expressions: @@ -234,7 +239,6 @@ def range(cls, frame, start, end=None, tz=None, limit=None): ''' - # FIXME Role of tz? _, frame_relative, relative_steps = cls._get_frames(frame) @@ -264,13 +268,18 @@ def span_range(cls, frame, start, end, tz=None, limit=None): :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). :param start: A datetime expression, the start of the range. :param end: (optional) A datetime expression, the end of the range. - :param tz: (optional) A :ref:`timezone expression `. Defaults to UTC. + :param tz: (optional) A :ref:`timezone expression `. Defaults to + ``start``'s timezone, or UTC if ``start`` is naive. :param limit: (optional) A maximum number of tuples to return. **NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to return the entire range. Call with ``limit`` alone to return a maximum # of results from the start. Call with both to cap a range at a maximum # of results. + **NOTE**: ``tz`` internally **replaces** the timezones of both ``start`` and ``end`` before + iterating. As such, either call with naive objects and ``tz``, or aware objects from the + same timezone and no ``tz``. + Supported frame values: year, quarter, month, week, day, hour, minute, second. Recognized datetime expressions: @@ -296,7 +305,7 @@ def span_range(cls, frame, start, end, tz=None, limit=None): (, ) ''' - # FIXME Role of tz? + tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) start = cls.fromdatetime(start, tzinfo).span(frame)[0] _range = cls.range(frame, start, end, tz, limit) From 26a6dd1a5ce5070f05ef69775ca8e05c11c31db6 Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Tue, 28 Jun 2016 16:44:37 -0400 Subject: [PATCH 042/649] Fix docs for a few more methods that accept timezone expressions --- arrow/arrow.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index a138b4078..0c3bfb000 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -104,7 +104,7 @@ def fromtimestamp(cls, timestamp, tzinfo=None): Timestamps should always be UTC. If you have a non-UTC timestamp:: - >>> arrow.Arrow.utcfromtimestamp(1367900664) .replace(tzinfo='US/Pacific') + >>> arrow.Arrow.utcfromtimestamp(1367900664).replace(tzinfo='US/Pacific') ''' @@ -133,10 +133,11 @@ def utcfromtimestamp(cls, timestamp): @classmethod def fromdatetime(cls, dt, tzinfo=None): ''' Constructs an :class:`Arrow ` object from a ``datetime`` and - optional replacement ``tzinfo`` object. + optional replacement timezone. :param dt: the ``datetime`` - :param tzinfo: (optional) a ``tzinfo`` object. Defaults to dt.tzinfo, or UTC if naive. + :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to ``dt``'s + timezone, or UTC if naive. If you only want to replace the timezone of naive datetimes:: @@ -155,10 +156,10 @@ def fromdatetime(cls, dt, tzinfo=None): @classmethod def fromdate(cls, date, tzinfo=None): ''' Constructs an :class:`Arrow ` object from a ``date`` and optional - replacement ``tzinfo`` object. Time values are set to 0. + replacement timezone. Time values are set to 0. :param date: the ``date`` - :param tzinfo: (optional) a ``tzinfo`` object. Defaults to UTC. + :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to UTC. ''' tzinfo = tzinfo or dateutil_tz.tzutc() @@ -172,12 +173,10 @@ def strptime(cls, date_str, fmt, tzinfo=None): :param date_str: the date string. :param fmt: the format string. - :param tzinfo: (optional) a ``tzinfo`` object. Defaults to the parsed timezone if found, - otherwise UTC. + :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to the parsed + timezone if ``fmt`` contains a timezone directive, otherwise UTC. ''' - # FIXME What if there's a %Z but it's missing in date_str? - # FIXME Document how to replace only if not present dt = datetime.strptime(date_str, fmt) tzinfo = tzinfo or dt.tzinfo or dateutil_tz.tzutc() @@ -315,11 +314,6 @@ def span_range(cls, frame, start, end, tz=None, limit=None): # representations def __repr__(self): - - dt = self._datetime - attrs = ', '.join([str(i) for i in [dt.year, dt.month, dt.day, dt.hour, dt.minute, - dt.second, dt.microsecond]]) - return '<{0} [{1}]>'.format(self.__class__.__name__, self.__str__()) def __str__(self): @@ -374,7 +368,8 @@ def datetime(self): @property def naive(self): - ''' Returns a naive datetime representation of the :class:`Arrow ` object. ''' + ''' Returns a naive datetime representation of the :class:`Arrow ` + object. ''' return self._datetime.replace(tzinfo=None) @@ -793,7 +788,8 @@ def time(self): return self._datetime.time() def timetz(self): - ''' Returns a ``time`` object with the same hour, minute, second, microsecond and tzinfo. ''' + ''' Returns a ``time`` object with the same hour, minute, second, microsecond and + tzinfo. ''' return self._datetime.timetz() From f290c12f5cf04c8998e5add0aec70bda195d2e7a Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Tue, 28 Jun 2016 16:48:19 -0400 Subject: [PATCH 043/649] Undo this one code change: I want this PR to be just about docs (although some dead code was removed) --- arrow/arrow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 0c3bfb000..75a2091e5 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -179,7 +179,7 @@ def strptime(cls, date_str, fmt, tzinfo=None): ''' dt = datetime.strptime(date_str, fmt) - tzinfo = tzinfo or dt.tzinfo or dateutil_tz.tzutc() + tzinfo = tzinfo or dt.tzinfo return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond, tzinfo) From c3bfd002db70a99afa597763b633004ed56dde47 Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Tue, 12 Jul 2016 12:02:43 +0200 Subject: [PATCH 044/649] return NotImplemented instead of raising TypeError TypeError will already be raised by Python, if both object implementations don't know how to handle the other type. This allows other classes to implement a __rOperation__, even when arrow doesn't know how to handle it with __Operation__ itself. Python 2 doesn't raise TypeError if binary arithmetic operations return NotImplemented, so we fixup this in __cmp__. From the docs: NotImplemented: Special value which should be returned by the binary special methods (e.g. __eq__(), __lt__(), __add__(), __rsub__(), etc.) to indicate that the operation is not implemented with respect to the other type; may be returned by the in-place binary special methods (e.g. __imul__(), __iand__(), etc.) for the same purpose. Its truth value is true. Binary arithmetic operations: [...] If one of those methods does not support the operation with the supplied arguments, it should return NotImplemented. Rich comparison methods: A rich comparison method may return the singleton NotImplemented if it does not implement the operation for a given pair of arguments. --- arrow/arrow.py | 29 ++++++++++++++++++----------- tests/arrow_tests.py | 6 +++--- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index b17c435cc..996406b81 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -681,7 +681,7 @@ def __add__(self, other): if isinstance(other, (timedelta, relativedelta)): return self.fromdatetime(self._datetime + other, self._datetime.tzinfo) - raise TypeError() + return NotImplemented def __radd__(self, other): return self.__add__(other) @@ -697,22 +697,18 @@ def __sub__(self, other): elif isinstance(other, Arrow): return self._datetime - other._datetime - raise TypeError() + return NotImplemented def __rsub__(self, other): if isinstance(other, datetime): return other - self._datetime - raise TypeError() + return NotImplemented # comparisons - def _cmperror(self, other): - raise TypeError('can\'t compare \'{0}\' to \'{1}\''.format( - type(self), type(other))) - def __eq__(self, other): if not isinstance(other, (Arrow, datetime)): @@ -723,36 +719,47 @@ def __eq__(self, other): return self._datetime == self._get_datetime(other) def __ne__(self, other): + + if not isinstance(other, (Arrow, datetime)): + return True + return not self.__eq__(other) def __gt__(self, other): if not isinstance(other, (Arrow, datetime)): - self._cmperror(other) + return NotImplemented return self._datetime > self._get_datetime(other) def __ge__(self, other): if not isinstance(other, (Arrow, datetime)): - self._cmperror(other) + return NotImplemented return self._datetime >= self._get_datetime(other) def __lt__(self, other): if not isinstance(other, (Arrow, datetime)): - self._cmperror(other) + return NotImplemented return self._datetime < self._get_datetime(other) def __le__(self, other): if not isinstance(other, (Arrow, datetime)): - self._cmperror(other) + return NotImplemented return self._datetime <= self._get_datetime(other) + def __cmp__(self, other): + if sys.version_info[0] < 3: # pragma: no cover + if not isinstance(other, (Arrow, datetime)): + raise TypeError('can\'t compare \'{0}\' to \'{1}\''.format( + type(self), type(other))) + + # datetime methods diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 7735b20e6..63f6623c8 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -272,7 +272,7 @@ def test_add_timedelta(self): def test_add_other(self): with assertRaises(TypeError): - self.arrow.__add__(1) + self.arrow + 1 def test_radd(self): @@ -301,7 +301,7 @@ def test_sub_arrow(self): def test_sub_other(self): with assertRaises(TypeError): - self.arrow.__sub__(object()) + self.arrow - object() def test_rsub_datetime(self): @@ -312,7 +312,7 @@ def test_rsub_datetime(self): def test_rsub_other(self): with assertRaises(TypeError): - self.arrow.__rsub__(timedelta(days=1)) + timedelta(days=1) - self.arrow class ArrowDatetimeInterfaceTests(Chai): From a835520e32e11a72a1f5f49fc02fb3112b734be4 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Sat, 30 Jul 2016 15:16:49 +0100 Subject: [PATCH 045/649] adds tox config file --- .gitignore | 3 +++ tox.ini | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index b40947c22..56d9632ab 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ dist/ docs/_build/ .idea .DS_Store + +# tox +.tox diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..cd97a9129 --- /dev/null +++ b/tox.ini @@ -0,0 +1,20 @@ +[tox] +envlist = py26,py27,py33,py34,py35 +skip_missing_interpreters = True + +[common] +deps = + nose + nose-cov + simplejson + +[testenv] +deps = + {[common]deps} + chai +commands = nosetests --all-modules --with-coverage arrow tests + +[testenv:py26] +deps = + {[common]deps} + chai==0.3.1 From f4bf660b99d2954654d3cf29bb06928135c72f92 Mon Sep 17 00:00:00 2001 From: Joseph Kahn Date: Tue, 16 Aug 2016 11:13:46 -0400 Subject: [PATCH 046/649] respect limit even if end is defined in a range --- arrow/arrow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index b17c435cc..cd49d81aa 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -898,7 +898,9 @@ def _get_iteration_params(cls, end, limit): return cls.max, limit else: - return end, sys.maxsize + if limit is None: + return end, sys.maxsize + return end, limit @staticmethod def _get_timestamp_from_input(timestamp): From c8e391ea3758e6bbd03b28afe26374dc356414e5 Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Tue, 23 Aug 2016 11:24:15 -0400 Subject: [PATCH 047/649] A copy/paste error for the new `shift` method resulted in incomplete test coverage --- tests/arrow_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 831167f24..27b39efce 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -511,7 +511,7 @@ class ArrowShiftTests(Chai): def test_not_attr(self): with assertRaises(AttributeError): - arrow.Arrow.utcnow().replace(abc=1) + arrow.Arrow.utcnow().shift(abc=1) def test_shift(self): From ec19991686d6fe78b09f27e972674b86e0ac6209 Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Tue, 23 Aug 2016 11:44:27 -0400 Subject: [PATCH 048/649] Re-introduce the "replace-shift" tests and get back to 100% code coverage --- tests/arrow_tests.py | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 27b39efce..045f93c7d 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -488,6 +488,38 @@ def test_replace(self): assertEqual(arw.replace(minute=1), arrow.Arrow(2013, 5, 5, 12, 1, 45)) assertEqual(arw.replace(second=1), arrow.Arrow(2013, 5, 5, 12, 30, 1)) + def test_replace_shift(self): + + arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) + + # This is all scheduled for deprecation + assertEqual(arw.replace(years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) + assertEqual(arw.replace(quarters=1), arrow.Arrow(2013, 8, 5, 12, 30, 45)) + assertEqual(arw.replace(quarters=1, months=1), arrow.Arrow(2013, 9, 5, 12, 30, 45)) + assertEqual(arw.replace(months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) + assertEqual(arw.replace(weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) + assertEqual(arw.replace(days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + assertEqual(arw.replace(hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) + assertEqual(arw.replace(minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) + assertEqual(arw.replace(seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) + assertEqual(arw.replace(microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1)) + + def test_replace_shift_negative(self): + + arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) + + # This is all scheduled for deprecation + assertEqual(arw.replace(years=-1), arrow.Arrow(2012, 5, 5, 12, 30, 45)) + assertEqual(arw.replace(quarters=-1), arrow.Arrow(2013, 2, 5, 12, 30, 45)) + assertEqual(arw.replace(quarters=-1, months=-1), arrow.Arrow(2013, 1, 5, 12, 30, 45)) + assertEqual(arw.replace(months=-1), arrow.Arrow(2013, 4, 5, 12, 30, 45)) + assertEqual(arw.replace(weeks=-1), arrow.Arrow(2013, 4, 28, 12, 30, 45)) + assertEqual(arw.replace(days=-1), arrow.Arrow(2013, 5, 4, 12, 30, 45)) + assertEqual(arw.replace(hours=-1), arrow.Arrow(2013, 5, 5, 11, 30, 45)) + assertEqual(arw.replace(minutes=-1), arrow.Arrow(2013, 5, 5, 12, 29, 45)) + assertEqual(arw.replace(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) + assertEqual(arw.replace(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999)) + def test_replace_tzinfo(self): arw = arrow.Arrow.utcnow().to('US/Eastern') @@ -501,6 +533,11 @@ def test_replace_week(self): with assertRaises(AttributeError): arrow.Arrow.utcnow().replace(week=1) + def test_replace_quarter(self): + + with assertRaises(AttributeError): + arrow.Arrow.utcnow().replace(quarter=1) + def test_replace_other_kwargs(self): with assertRaises(AttributeError): @@ -518,18 +555,23 @@ def test_shift(self): arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) assertEqual(arw.shift(years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) + # assertEqual(arw.shift(quarters=1), arrow.Arrow(2013, 8, 5, 12, 30, 45)) + # assertEqual(arw.shift(quarters=1, months=1), arrow.Arrow(2013, 9, 5, 12, 30, 45)) assertEqual(arw.shift(months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) assertEqual(arw.shift(weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) assertEqual(arw.shift(days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) assertEqual(arw.shift(hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) assertEqual(arw.shift(minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) assertEqual(arw.shift(seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) + assertEqual(arw.shift(microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1)) def test_shift_negative(self): arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) assertEqual(arw.shift(years=-1), arrow.Arrow(2012, 5, 5, 12, 30, 45)) + # assertEqual(arw.shift(quarters=-1), arrow.Arrow(2013, 2, 5, 12, 30, 45)) + # assertEqual(arw.shift(quarters=-1, months=-1), arrow.Arrow(2013, 1, 5, 12, 30, 45)) assertEqual(arw.shift(months=-1), arrow.Arrow(2013, 4, 5, 12, 30, 45)) assertEqual(arw.shift(weeks=-1), arrow.Arrow(2013, 4, 28, 12, 30, 45)) assertEqual(arw.shift(days=-1), arrow.Arrow(2013, 5, 4, 12, 30, 45)) @@ -538,7 +580,6 @@ def test_shift_negative(self): assertEqual(arw.shift(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) assertEqual(arw.shift(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999)) - class ArrowRangeTests(Chai): def test_year(self): From b5eee6ddf91f6479309a8597f3052a0f8ffa37a0 Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Tue, 23 Aug 2016 11:59:49 -0400 Subject: [PATCH 049/649] Bug: `replace` was using the value of the last-read argument for the `quarters` argument --- arrow/arrow.py | 9 +++------ tests/arrow_tests.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 6483e510e..dc4bca3bb 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -423,12 +423,9 @@ def replace(self, **kwargs): raise AttributeError('unknown attribute: "{0}"'.format(key)) # core datetime does not support quarters, translate to months. - if 'quarters' in relative_kwargs.keys(): - if relative_kwargs.get('months') is None: - relative_kwargs['months'] = 0 - relative_kwargs['months'] += (value * self._MONTHS_PER_QUARTER) - relative_kwargs.pop('quarters') - + relative_kwargs.setdefault('months', 0) + relative_kwargs['months'] += relative_kwargs.pop('quarters', 0) * self._MONTHS_PER_QUARTER + current = self._datetime.replace(**absolute_kwargs) current += relativedelta(**relative_kwargs) # TODO: DEPRECATED diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 045f93c7d..2c2ccdcfd 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -520,6 +520,22 @@ def test_replace_shift_negative(self): assertEqual(arw.replace(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) assertEqual(arw.replace(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999)) + def test_replace_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. + # Recall that the keyword argument dict, like all dicts, is unordered, so only certain + # combinations of arguments would exhibit this. + assertEqual(arw.replace(quarters=0, years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) + assertEqual(arw.replace(quarters=0, months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) + assertEqual(arw.replace(quarters=0, weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) + assertEqual(arw.replace(quarters=0, days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + assertEqual(arw.replace(quarters=0, hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) + assertEqual(arw.replace(quarters=0, minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) + assertEqual(arw.replace(quarters=0, seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) + assertEqual(arw.replace(quarters=0, microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1)) + def test_replace_tzinfo(self): arw = arrow.Arrow.utcnow().to('US/Eastern') @@ -580,6 +596,22 @@ def test_shift_negative(self): assertEqual(arw.shift(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) assertEqual(arw.shift(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999)) + # 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. + # # Recall that the keyword argument dict, like all dicts, is unordered, so only certain + # # combinations of arguments would exhibit this. + # assertEqual(arw.replace(quarters=0, years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) + # assertEqual(arw.replace(quarters=0, months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) + # assertEqual(arw.replace(quarters=0, weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) + # assertEqual(arw.replace(quarters=0, days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + # assertEqual(arw.replace(quarters=0, hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) + # assertEqual(arw.replace(quarters=0, minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) + # assertEqual(arw.replace(quarters=0, seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) + # assertEqual(arw.replace(quarters=0, microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1)) + class ArrowRangeTests(Chai): def test_year(self): From c99b58fec66f79022afc8ae6c3d2b6cb98fec2b9 Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Tue, 23 Aug 2016 12:50:06 -0400 Subject: [PATCH 050/649] Support `quarters` in `shift` --- arrow/arrow.py | 13 ++++++++----- tests/arrow_tests.py | 38 +++++++++++++++++++------------------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index dc4bca3bb..51e6472e2 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -389,8 +389,7 @@ def replace(self, **kwargs): >>> arw.replace(tzinfo=tz.tzlocal()) - NOTE: Deprecated in next release - Use plural property names to shift their current value relatively: + Use plural property names to shift their current value relatively (**deprecated**): >>> arw.replace(years=1, months=-1) @@ -438,8 +437,8 @@ def replace(self, **kwargs): return self.fromdatetime(current) def shift(self, **kwargs): - ''' Returns a new :class:`Arrow ` object with - attributes updated according to inputs. + ''' Returns a new :class:`Arrow ` object with attributes updated + according to inputs. Use plural property names to shift their current value relatively: @@ -456,11 +455,15 @@ def shift(self, **kwargs): for key, value in kwargs.items(): - if key in self._ATTRS_PLURAL or key == 'weeks': + if key in self._ATTRS_PLURAL or key in ['weeks', 'quarters']: relative_kwargs[key] = value else: raise AttributeError() + # core datetime does not support quarters, translate to months. + relative_kwargs.setdefault('months', 0) + relative_kwargs['months'] += relative_kwargs.pop('quarters', 0) * self._MONTHS_PER_QUARTER + current = self._datetime + relativedelta(**relative_kwargs) return self.fromdatetime(current) diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 2c2ccdcfd..7d5cf9901 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -571,8 +571,8 @@ def test_shift(self): arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) assertEqual(arw.shift(years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) - # assertEqual(arw.shift(quarters=1), arrow.Arrow(2013, 8, 5, 12, 30, 45)) - # assertEqual(arw.shift(quarters=1, months=1), arrow.Arrow(2013, 9, 5, 12, 30, 45)) + assertEqual(arw.shift(quarters=1), arrow.Arrow(2013, 8, 5, 12, 30, 45)) + assertEqual(arw.shift(quarters=1, months=1), arrow.Arrow(2013, 9, 5, 12, 30, 45)) assertEqual(arw.shift(months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) assertEqual(arw.shift(weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) assertEqual(arw.shift(days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) @@ -586,8 +586,8 @@ def test_shift_negative(self): arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) assertEqual(arw.shift(years=-1), arrow.Arrow(2012, 5, 5, 12, 30, 45)) - # assertEqual(arw.shift(quarters=-1), arrow.Arrow(2013, 2, 5, 12, 30, 45)) - # assertEqual(arw.shift(quarters=-1, months=-1), arrow.Arrow(2013, 1, 5, 12, 30, 45)) + assertEqual(arw.shift(quarters=-1), arrow.Arrow(2013, 2, 5, 12, 30, 45)) + assertEqual(arw.shift(quarters=-1, months=-1), arrow.Arrow(2013, 1, 5, 12, 30, 45)) assertEqual(arw.shift(months=-1), arrow.Arrow(2013, 4, 5, 12, 30, 45)) assertEqual(arw.shift(weeks=-1), arrow.Arrow(2013, 4, 28, 12, 30, 45)) assertEqual(arw.shift(days=-1), arrow.Arrow(2013, 5, 4, 12, 30, 45)) @@ -596,21 +596,21 @@ def test_shift_negative(self): assertEqual(arw.shift(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) assertEqual(arw.shift(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999)) - # 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. - # # Recall that the keyword argument dict, like all dicts, is unordered, so only certain - # # combinations of arguments would exhibit this. - # assertEqual(arw.replace(quarters=0, years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) - # assertEqual(arw.replace(quarters=0, months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) - # assertEqual(arw.replace(quarters=0, weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) - # assertEqual(arw.replace(quarters=0, days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - # assertEqual(arw.replace(quarters=0, hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) - # assertEqual(arw.replace(quarters=0, minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) - # assertEqual(arw.replace(quarters=0, seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) - # assertEqual(arw.replace(quarters=0, microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1)) + 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. + # Recall that the keyword argument dict, like all dicts, is unordered, so only certain + # combinations of arguments would exhibit this. + assertEqual(arw.replace(quarters=0, years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) + assertEqual(arw.replace(quarters=0, months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) + assertEqual(arw.replace(quarters=0, weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) + assertEqual(arw.replace(quarters=0, days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + assertEqual(arw.replace(quarters=0, hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) + assertEqual(arw.replace(quarters=0, minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) + assertEqual(arw.replace(quarters=0, seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) + assertEqual(arw.replace(quarters=0, microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1)) class ArrowRangeTests(Chai): From 4cca70ec9e427fddbcabd8c413b560194ea9e66e Mon Sep 17 00:00:00 2001 From: Mihai Costea Date: Fri, 30 Sep 2016 11:29:15 +0300 Subject: [PATCH 051/649] Add Romanian locale --- arrow/locales.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 5af426746..cf5802972 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1786,4 +1786,33 @@ class RomanshLocale(Locale): ] +class RomanianLocale(Locale): + names = ['ro', 'ro_ro'] + + past = 'în urmă cu {0}' + future = 'în {0}' + + timeframes = { + 'now': 'acum', + 'seconds': 'secunde', + 'minute': 'minut', + 'minutes': '{0} minute', + 'hour': 'oră', + 'hours': '{0} ure', + 'day': 'zi', + 'days': '{0} zile', + 'month': 'lună', + 'months': '{0} luni', + 'year': 'an', + 'years': '{0} ani', + } + + month_names = ['', 'ianuarie', 'februarie', 'martie', 'aprilie', 'mai', 'iunie', 'iulie', + 'august', 'septembrie', 'octombrie', 'noiembrie', 'decembrie'] + month_abbreviations = ['', 'ian', 'febr', 'mart', 'apr', 'mai', 'iun', 'iul', 'aug', 'sept', 'oct', 'nov', 'dec'] + + day_names = ['', 'luni', 'marți', 'miercuri', 'joi', 'vineri', 'sâmbătă', 'duminică'] + day_abbreviations = ['', 'Lun', 'Mar', 'Mie', 'Joi', 'Vin', 'Sâm', 'Dum'] + + _locales = _map_locales() From 9115858ed337ad11807db7c98dff67f12f57a0b2 Mon Sep 17 00:00:00 2001 From: Mihai Costea Date: Fri, 30 Sep 2016 12:32:54 +0300 Subject: [PATCH 052/649] Update singular Romanian names --- arrow/locales.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index cf5802972..6450c04d9 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1789,26 +1789,26 @@ class RomanshLocale(Locale): class RomanianLocale(Locale): names = ['ro', 'ro_ro'] - past = 'în urmă cu {0}' - future = 'în {0}' + past = '{0} în urmă' + future = 'peste {0}' timeframes = { 'now': 'acum', - 'seconds': 'secunde', - 'minute': 'minut', + 'seconds': 'câteva secunde', + 'minute': 'un minut', 'minutes': '{0} minute', - 'hour': 'oră', - 'hours': '{0} ure', - 'day': 'zi', + 'hour': 'o oră', + 'hours': '{0} ore', + 'day': 'o zi', 'days': '{0} zile', - 'month': 'lună', + 'month': 'o lună', 'months': '{0} luni', - 'year': 'an', + 'year': 'un an', 'years': '{0} ani', } month_names = ['', 'ianuarie', 'februarie', 'martie', 'aprilie', 'mai', 'iunie', 'iulie', - 'august', 'septembrie', 'octombrie', 'noiembrie', 'decembrie'] + 'august', 'septembrie', 'octombrie', 'noiembrie', 'decembrie'] month_abbreviations = ['', 'ian', 'febr', 'mart', 'apr', 'mai', 'iun', 'iul', 'aug', 'sept', 'oct', 'nov', 'dec'] day_names = ['', 'luni', 'marți', 'miercuri', 'joi', 'vineri', 'sâmbătă', 'duminică'] From 2801388e6aca76452cc0a0fd38d9bf4f31c774dc Mon Sep 17 00:00:00 2001 From: Mihai Costea Date: Fri, 30 Sep 2016 12:33:12 +0300 Subject: [PATCH 053/649] Add Romanian locale tests --- tests/locales_tests.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 28bffae1c..cde56f43d 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -474,3 +474,35 @@ def test_ordinal_number(self): assertEqual(result11, '11তম') assertEqual(result42, '42তম') assertEqual(self.locale._ordinal_number(-1), None) + + +class RomanianLocaleTests(Chai): + + def setUp(self): + super(RomanianLocaleTests, self).setUp() + + self.locale = locales.RomanianLocale() + + def test_timeframes(self): + + self.assertEqual(self.locale._format_timeframe('hours', 2), '2 ore') + self.assertEqual(self.locale._format_timeframe('months', 2), '2 luni') + + self.assertEqual(self.locale._format_timeframe('days', 2), '2 zile') + self.assertEqual(self.locale._format_timeframe('years', 2), '2 ani') + + self.assertEqual(self.locale._format_timeframe('hours', 3), '3 ore') + self.assertEqual(self.locale._format_timeframe('months', 4), '4 luni') + self.assertEqual(self.locale._format_timeframe('days', 3), '3 zile') + self.assertEqual(self.locale._format_timeframe('years', 5), '5 ani') + + def test_relative_timeframes(self): + self.assertEqual(self.locale._format_relative("acum", "now", 0), "acum") + self.assertEqual(self.locale._format_relative("o oră", "hour", 1), "peste o oră") + self.assertEqual(self.locale._format_relative("o oră", "hour", -1), "o oră în urmă") + self.assertEqual(self.locale._format_relative("un minut", "minute", 1), "peste un minut") + self.assertEqual(self.locale._format_relative("un minut", "minute", -1), "un minut în urmă") + self.assertEqual(self.locale._format_relative("câteva secunde", "seconds", -1), "câteva secunde în urmă") + self.assertEqual(self.locale._format_relative("câteva secunde", "seconds", 1), "peste câteva secunde") + self.assertEqual(self.locale._format_relative("o zi", "day", -1), "o zi în urmă") + self.assertEqual(self.locale._format_relative("o zi", "day", 1), "peste o zi") From d201cbd3f52be8c6b7f547ec56ce546cc3575733 Mon Sep 17 00:00:00 2001 From: Jose Riha Date: Wed, 12 Oct 2016 12:53:38 +0200 Subject: [PATCH 054/649] Add Slovak locale --- arrow/locales.py | 89 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 8 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 5af426746..0b944d29d 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -919,7 +919,7 @@ class NewNorwegianLocale(Locale): class PortugueseLocale(Locale): names = ['pt', 'pt_pt'] - + past = 'há {0}' future = 'em {0}' @@ -946,11 +946,11 @@ class PortugueseLocale(Locale): day_names = ['', 'segunda-feira', 'terça-feira', 'quarta-feira', 'quinta-feira', 'sexta-feira', 'sábado', 'domingo'] day_abbreviations = ['', 'seg', 'ter', 'qua', 'qui', 'sex', 'sab', 'dom'] - - + + class BrazilianPortugueseLocale(PortugueseLocale): names = ['pt_br'] - + past = 'fazem {0}' @@ -1284,7 +1284,8 @@ class CzechLocale(Locale): def _format_timeframe(self, timeframe, delta): - '''Czech aware time frame format function, takes into account the differences between past and future forms.''' + '''Czech aware time frame format function, takes into account + the differences between past and future forms.''' form = self.timeframes[timeframe] if isinstance(form, dict): if delta == 0: @@ -1293,7 +1294,78 @@ def _format_timeframe(self, timeframe, delta): form = form['future'] else: form = form['past'] - delta = abs(delta) + delta = abs(delta) + + if isinstance(form, list): + if 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): + form = form[0] + else: + form = form[1] + + return form.format(delta) + + +class SlovakLocale(Locale): + names = ['sk', 'sk_sk'] + + timeframes = { + 'now': 'Teraz', + 'seconds': { + 'past': 'pár sekundami', + 'future': ['{0} sekundy', '{0} sekúnd'] + }, + 'minute': {'past': 'minútou', 'future': 'minútu', 'zero': '{0} minút'}, + 'minutes': { + 'past': '{0} minútami', + 'future': ['{0} minúty', '{0} minút'] + }, + 'hour': {'past': 'hodinou', 'future': 'hodinu', 'zero': '{0} hodín'}, + 'hours': { + 'past': '{0} hodinami', + 'future': ['{0} hodiny', '{0} hodín'] + }, + 'day': {'past': 'dňom', 'future': 'deň', 'zero': '{0} dní'}, + 'days': { + 'past': '{0} dňami', + 'future': ['{0} dni', '{0} dní'] + }, + 'month': {'past': 'mesiacom', 'future': 'mesiac', 'zero': '{0} mesiacov'}, + 'months': { + 'past': '{0} mesiacmi', + 'future': ['{0} mesiace', '{0} mesiacov'] + }, + 'year': {'past': 'rokom', 'future': 'rok', 'zero': '{0} rokov'}, + 'years': { + 'past': '{0} rokmi', + 'future': ['{0} roky', '{0} rokov'] + } + } + + past = 'Pred {0}' + future = 'O {0}' + + month_names = ['', 'január', 'február', 'marec', 'apríl', 'máj', 'jún', + 'júl', 'august', 'september', 'október', 'november', 'december'] + month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'máj', 'jún', 'júl', + 'aug', 'sep', 'okt', 'nov', 'dec'] + + day_names = ['', 'pondelok', 'utorok', 'streda', 'štvrtok', 'piatok', + 'sobota', 'nedeľa'] + day_abbreviations = ['', 'po', 'ut', 'st', 'št', 'pi', 'so', 'ne'] + + + def _format_timeframe(self, timeframe, delta): + '''Slovak aware time frame format function, takes into account + the differences between past and future forms.''' + form = self.timeframes[timeframe] + if isinstance(form, dict): + if delta == 0: + form = form['zero'] # And *never* use 0 in the singular! + elif delta > 0: + form = form['future'] + else: + form = form['past'] + delta = abs(delta) if isinstance(form, list): if 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): @@ -1303,6 +1375,7 @@ def _format_timeframe(self, timeframe, delta): return form.format(delta) + class FarsiLocale(Locale): names = ['fa', 'fa_ir'] @@ -1463,7 +1536,7 @@ class MarathiLocale(Locale): day_names = ['', 'सोमवार', 'मंगळवार', 'बुधवार', 'गुरुवार', 'शुक्रवार', 'शनिवार', 'रविवार'] day_abbreviations = ['', 'सोम', 'मंगळ', 'बुध', 'गुरु', 'शुक्र', 'शनि', 'रवि'] - + def _map_locales(): locales = {} @@ -1471,7 +1544,7 @@ def _map_locales(): for cls_name, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): if issubclass(cls, Locale): for name in cls.names: - locales[name.lower()] = cls + locales[name.lower()] = cls return locales From f2c5c9c1144f2a20bbc5183ef31e7ef4997f9216 Mon Sep 17 00:00:00 2001 From: jose1711 Date: Fri, 14 Oct 2016 09:02:43 +0200 Subject: [PATCH 055/649] Add Slovak locale tests --- tests/locales_tests.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 28bffae1c..9de6fa873 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -270,6 +270,38 @@ def test_format_relative_past(self): assertEqual(result, 'Před hodinou') +class SlovakLocaleTests(Chai): + + def setUp(self): + super(SlovakLocaleTests, self).setUp() + + self.locale = locales.SlovakLocale() + + def test_format_timeframe(self): + + assertEqual(self.locale._format_timeframe('hours', 2), '2 hodiny') + assertEqual(self.locale._format_timeframe('hours', 5), '5 hodín') + assertEqual(self.locale._format_timeframe('hour', 0), '0 hodín') + assertEqual(self.locale._format_timeframe('hours', -2), '2 hodinami') + assertEqual(self.locale._format_timeframe('hours', -5), '5 hodinami') + assertEqual(self.locale._format_timeframe('now', 0), 'Teraz') + + def test_format_relative_now(self): + + result = self.locale._format_relative('Teraz', 'now', 0) + assertEqual(result, 'Teraz') + + def test_format_relative_future(self): + + result = self.locale._format_relative('hodinu', 'hour', 1) + assertEqual(result, 'O hodinu') + + def test_format_relative_past(self): + + result = self.locale._format_relative('hodinou', 'hour', -1) + assertEqual(result, 'Pred hodinou') + + class BulgarianLocaleTests(Chai): def test_plurals2(self): From 2dc36f472fc16d11a763bb931cc40c31f2f94f86 Mon Sep 17 00:00:00 2001 From: Gregor Hren Date: Tue, 15 Nov 2016 09:17:30 +0100 Subject: [PATCH 056/649] Slovenian locale added --- arrow/locales.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 5af426746..f27f8751e 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1786,4 +1786,51 @@ class RomanshLocale(Locale): ] +class SlovenianLocale(Locale): + names = ['sl', 'sl_si'] + + past = 'pred {0}' + future = 'čez {0}' + + timeframes = { + 'now': 'zdaj', + 'seconds': 'sekund', + 'minute': 'minuta', + 'minutes': '{0} minutami', + 'hour': 'uro', + 'hours': '{0} ur', + 'day': 'dan', + 'days': '{0} dni', + 'month': 'mesec', + 'months': '{0} mesecev', + 'year': 'leto', + 'years': '{0} let', + } + + meridians = { + 'am': '', + 'pm': '', + 'AM': '', + 'PM': '', + } + + month_names = [ + '', 'Januar', 'Februar', 'Marec', 'April', 'Maj', 'Junij', 'Julij', + 'Avgust', 'September', 'Oktober', 'November', 'December' + ] + + month_abbreviations = [ + '', 'Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Avg', + 'Sep', 'Okt', 'Nov', 'Dec' + ] + + day_names = [ + '', 'Ponedeljek', 'Torek', 'Sreda', 'Četrtek', 'Petek', 'Sobota', 'Nedelja' + ] + + day_abbreviations = [ + '', 'Pon', 'Tor', 'Sre', 'Čet', 'Pet', 'Sob', 'Ned' + ] + + _locales = _map_locales() From e0d408a4c27c2ea980f1e0d3fcc9ed92696ec49f Mon Sep 17 00:00:00 2001 From: James Tocknell Date: Tue, 15 Nov 2016 22:03:42 +1100 Subject: [PATCH 057/649] Add support for universal wheels --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index dbf2184b1..6af300877 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,3 +9,6 @@ cover-package = arrow cover-erase = true cover-inclusive = true cover-branches = true + +[bdist_wheel] +universal=1 From aef1ce018c60bfeb2e0910dd293cfc3131fa87a9 Mon Sep 17 00:00:00 2001 From: Andrew Elkins Date: Wed, 16 Nov 2016 16:35:08 -0800 Subject: [PATCH 058/649] Remove duplicate code This will close #306 --- arrow/arrow.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index b17c435cc..b0de6035b 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -718,8 +718,6 @@ def __eq__(self, other): if not isinstance(other, (Arrow, datetime)): return False - other = self._get_datetime(other) - return self._datetime == self._get_datetime(other) def __ne__(self, other): From bc03b0b5337642c6766cda4f483074e0ecc88455 Mon Sep 17 00:00:00 2001 From: Buck Evan Date: Wed, 16 Nov 2016 09:52:52 -0800 Subject: [PATCH 059/649] fix parsing of iso-8601 dates as emitted by GNU date -Ins --- arrow/parser.py | 37 ++++++++-------------- tests/parser_tests.py | 72 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 75 insertions(+), 34 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 6e94a10c4..480738216 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -14,13 +14,10 @@ class ParserError(RuntimeError): class DateTimeParser(object): - _FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X)') + _FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|X)') _ESCAPE_RE = re.compile('\[[^\[\]]*\]') - _ONE_THROUGH_SIX_DIGIT_RE = re.compile('\d{1,6}') - _ONE_THROUGH_FIVE_DIGIT_RE = re.compile('\d{1,5}') - _ONE_THROUGH_FOUR_DIGIT_RE = re.compile('\d{1,4}') - _ONE_TWO_OR_THREE_DIGIT_RE = re.compile('\d{1,3}') + _ONE_OR_MORE_DIGIT_RE = re.compile('\d+') _ONE_OR_TWO_DIGIT_RE = re.compile('\d{1,2}') _FOUR_DIGIT_RE = re.compile('\d{4}') _TWO_DIGIT_RE = re.compile('\d{2}') @@ -47,12 +44,7 @@ class DateTimeParser(object): 'ZZZ': _TZ_NAME_RE, 'ZZ': _TZ_RE, 'Z': _TZ_RE, - 'SSSSSS': _ONE_THROUGH_SIX_DIGIT_RE, - 'SSSSS': _ONE_THROUGH_FIVE_DIGIT_RE, - 'SSSS': _ONE_THROUGH_FOUR_DIGIT_RE, - 'SSS': _ONE_TWO_OR_THREE_DIGIT_RE, - 'SS': _ONE_OR_TWO_DIGIT_RE, - 'S': re.compile('\d'), + 'S': _ONE_OR_MORE_DIGIT_RE, } MARKERS = ['YYYY', 'MM', 'DD'] @@ -92,11 +84,10 @@ def parse_iso(self, string): time_parts = re.split('[+-]', time_string, 1) has_tz = len(time_parts) > 1 has_seconds = time_parts[0].count(':') > 1 - has_subseconds = '.' in time_parts[0] + has_subseconds = re.search('[.,]', time_parts[0]) if has_subseconds: - subseconds_token = 'S' * min(len(re.split('\D+', time_parts[0].split('.')[1], 1)[0]), 6) - formats = ['YYYY-MM-DDTHH:mm:ss.%s' % subseconds_token] + formats = ['YYYY-MM-DDTHH:mm:ss%sS' % has_subseconds.group()] elif has_seconds: formats = ['YYYY-MM-DDTHH:mm:ss'] else: @@ -132,6 +123,8 @@ def parse(self, string, fmt): # Extract the bracketed expressions to be reinserted later. escaped_fmt = re.sub(self._ESCAPE_RE, "#" , fmt) + # Any number of S is the same as one. + escaped_fmt = re.sub('S+', 'S', escaped_fmt) escaped_data = re.findall(self._ESCAPE_RE, fmt) fmt_pattern = escaped_fmt @@ -202,18 +195,12 @@ def _parse_token(self, token, value, parts): elif token in ['ss', 's']: parts['second'] = int(value) - elif token == 'SSSSSS': - parts['microsecond'] = int(value) - elif token == 'SSSSS': - parts['microsecond'] = int(value) * 10 - elif token == 'SSSS': - parts['microsecond'] = int(value) * 100 - elif token == 'SSS': - parts['microsecond'] = int(value) * 1000 - elif token == 'SS': - parts['microsecond'] = int(value) * 10000 elif token == 'S': - parts['microsecond'] = int(value) * 100000 + # We have the *most significant* digits of an arbitrary-precision integer. + # We want the six most significant digits as an integer, rounded. + # FIXME: add nanosecond support somehow? + value = value.ljust(7, '0') + parts['microsecond'] = int(value[:6]) elif token == 'X': parts['timestamp'] = int(value) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 59a84dbc9..5eb6da10a 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -188,17 +188,34 @@ def test_parse_subsecond(self): assertEqual(self.parser.parse('2013-01-01 12:30:45.987654', 'YYYY-MM-DD HH:mm:ss.SSSSSS'), expected) assertEqual(self.parser.parse_iso('2013-01-01 12:30:45.987654'), expected) + def test_parse_subsecond_rounding(self): + """currently, we've decided there's no rounding""" + format = 'YYYY-MM-DD HH:mm:ss.S' + + # round up + string = '2013-01-01 12:30:45.9876539' + expected = datetime(2013, 1, 1, 12, 30, 45, 987653) + assertEqual(self.parser.parse(string, format), expected) + assertEqual(self.parser.parse_iso(string), expected) + + # round down + string = '2013-01-01 12:30:45.98765432' expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - assertEqual(self.parser.parse('2013-01-01 12:30:45.9876543', 'YYYY-MM-DD HH:mm:ss.SSSSSS'), expected) - assertEqual(self.parser.parse_iso('2013-01-01 12:30:45.9876543'), expected) - - expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - assertEqual(self.parser.parse('2013-01-01 12:30:45.98765432', 'YYYY-MM-DD HH:mm:ss.SSSSSS'), expected) - assertEqual(self.parser.parse_iso('2013-01-01 12:30:45.98765432'), expected) - + assertEqual(self.parser.parse(string, format), expected) + #import pudb; pudb.set_trace() + assertEqual(self.parser.parse_iso(string), expected) + + # round half-up + string = '2013-01-01 12:30:45.987653521' + expected = datetime(2013, 1, 1, 12, 30, 45, 987653) + assertEqual(self.parser.parse(string, format), expected) + assertEqual(self.parser.parse_iso(string), expected) + + # round half-down + string = '2013-01-01 12:30:45.9876545210' expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - assertEqual(self.parser.parse('2013-01-01 12:30:45.987654321', 'YYYY-MM-DD HH:mm:ss.SSSSSS'), expected) - assertEqual(self.parser.parse_iso('2013-01-01 12:30:45.987654321'), expected) + assertEqual(self.parser.parse(string, format), expected) + assertEqual(self.parser.parse_iso(string), expected) def test_map_lookup_keyerror(self): @@ -398,6 +415,21 @@ def test_YYYY_MM_DDTHH_mm_ss_S(self): datetime(2013, 2, 3, 4, 5, 6, 789120) ) + # ISO 8601:2004(E), ISO, 2004-12-01, 4.2.2.4 ... the decimal fraction + # shall be divided from the integer part by the decimal sign specified + # in ISO 31-0, i.e. the comma [,] or full stop [.]. Of these, the comma + # is the preferred sign. + assertEqual( + self.parser.parse_iso('2013-02-03T04:05:06,789123678'), + datetime(2013, 2, 3, 4, 5, 6, 789123) + ) + + # there is no limit on the number of decimal places + assertEqual( + self.parser.parse_iso('2013-02-03T04:05:06.789123678'), + datetime(2013, 2, 3, 4, 5, 6, 789123) + ) + def test_YYYY_MM_DDTHH_mm_ss_SZ(self): assertEqual( @@ -431,6 +463,28 @@ def test_YYYY_MM_DDTHH_mm_ss_SZ(self): datetime(2013, 2, 3, 4, 5, 6, 789120) ) + def test_gnu_date(self): + """ + regression tests for parsing output from GNU date(1) + """ + # date -Ins + assertEqual( + self.parser.parse_iso('2016-11-16T09:46:30,895636557-0800'), + datetime( + 2016, 11, 16, 9, 46, 30, 895636, + tzinfo=tz.tzoffset(None, -3600 * 8), + ) + ) + + # date --rfc-3339=ns + assertEqual( + self.parser.parse_iso('2016-11-16 09:51:14.682141526-08:00'), + datetime( + 2016, 11, 16, 9, 51, 14, 682141, + tzinfo=tz.tzoffset(None, -3600 * 8), + ) + ) + def test_isoformat(self): dt = datetime.utcnow() From 3288883a975f5ffaddd1fbe3fca31b6bf7f90898 Mon Sep 17 00:00:00 2001 From: Buck Evan Date: Wed, 16 Nov 2016 13:34:26 -0800 Subject: [PATCH 060/649] check coverage for test code too this is useful; i was able to find two tests that weren't even running also, show the lines that are missing coverage, if any --- .coveragerc | 8 +++++++- setup.cfg | 6 +++--- tests/arrow_tests.py | 2 +- tests/factory_tests.py | 7 ++++++- tests/util_tests.py | 2 +- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/.coveragerc b/.coveragerc index ad3153683..1e8b639e2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,9 @@ [run] branch = True -source = arrow +source = + tests + arrow + +[report] +show_missing = True +fail_under = 100 diff --git a/setup.cfg b/setup.cfg index 6af300877..53e78d1e2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,10 +5,10 @@ verbosity = 2 all-modules = true with-coverage = true cover-min-percentage = 100 -cover-package = arrow +cover-package = + arrow + tests cover-erase = true -cover-inclusive = true -cover-branches = true [bdist_wheel] universal=1 diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 7735b20e6..aada4f9fd 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -900,7 +900,7 @@ def test_span_second(self): assertEqual(floor, datetime(2013, 2, 15, 3, 41, 22, tzinfo=tz.tzutc())) assertEqual(ceil, datetime(2013, 2, 15, 3, 41, 22, 999999, tzinfo=tz.tzutc())) - def test_span_hour(self): + def test_span_microsecond(self): floor, ceil = self.arrow.span('microsecond') diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 7de47d6d5..603ac799e 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -180,7 +180,12 @@ def test_three_args(self): assertEqual(self.factory.get(2013, 1, 1), datetime(2013, 1, 1, tzinfo=tz.tzutc())) -def UtcNowTests(Chai): +class UtcNowTests(Chai): + + def setUp(self): + super(UtcNowTests, self).setUp() + + self.factory = factory.ArrowFactory() def test_utcnow(self): diff --git a/tests/util_tests.py b/tests/util_tests.py index 7be123014..c3d059fdd 100644 --- a/tests/util_tests.py +++ b/tests/util_tests.py @@ -18,7 +18,7 @@ def test_total_seconds_26(self): assertEqual(util._total_seconds_26(td), 30) - if util.version >= '2.7': + if util.version >= '2.7': # pragma: no cover def test_total_seconds_27(self): From 9355b404c9745b1b697ff4efc955aabb8324ea93 Mon Sep 17 00:00:00 2001 From: Buck Evan Date: Wed, 16 Nov 2016 14:05:27 -0800 Subject: [PATCH 061/649] fix py2, too, and don't swallow unexpected exceptions --- arrow/parser.py | 4 ++-- tests/parser_tests.py | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 480738216..0805f5ce3 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -199,7 +199,7 @@ def _parse_token(self, token, value, parts): # We have the *most significant* digits of an arbitrary-precision integer. # We want the six most significant digits as an integer, rounded. # FIXME: add nanosecond support somehow? - value = value.ljust(7, '0') + value = value.ljust(7, str('0')) parts['microsecond'] = int(value[:6]) elif token == 'X': @@ -250,7 +250,7 @@ def _parse_multiformat(self, string, formats): try: _datetime = self.parse(string, fmt) break - except: + except ParserError: pass if _datetime is None: diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 5eb6da10a..1dd7d64ea 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -22,7 +22,7 @@ def test_parse_multiformat(self): mock_datetime = mock() - expect(self.parser.parse).args('str', 'fmt_a').raises(Exception) + expect(self.parser.parse).args('str', 'fmt_a').raises(ParserError) expect(self.parser.parse).args('str', 'fmt_b').returns(mock_datetime) result = self.parser._parse_multiformat('str', ['fmt_a', 'fmt_b']) @@ -31,10 +31,20 @@ def test_parse_multiformat(self): def test_parse_multiformat_all_fail(self): - expect(self.parser.parse).args('str', 'fmt_a').raises(Exception) - expect(self.parser.parse).args('str', 'fmt_b').raises(Exception) + expect(self.parser.parse).args('str', 'fmt_a').raises(ParserError) + expect(self.parser.parse).args('str', 'fmt_b').raises(ParserError) - with assertRaises(Exception): + with assertRaises(ParserError): + self.parser._parse_multiformat('str', ['fmt_a', 'fmt_b']) + + def test_parse_multiformat_unexpected_fail(self): + + class UnexpectedError(Exception): + pass + + expect(self.parser.parse).args('str', 'fmt_a').raises(UnexpectedError) + + with assertRaises(UnexpectedError): self.parser._parse_multiformat('str', ['fmt_a', 'fmt_b']) def test_parse_token_nonsense(self): From 8ec8f89632527a8510ae2f227dae12eb37312cac Mon Sep 17 00:00:00 2001 From: Buck Evan Date: Wed, 16 Nov 2016 16:30:13 -0800 Subject: [PATCH 062/649] fix all (seven) sphinx warnings mainly these are misnaming of classes' modules --- arrow/api.py | 2 +- arrow/arrow.py | 1 + arrow/locales.py | 4 ++-- docs/conf.py | 4 ++-- docs/index.rst | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/arrow/api.py b/arrow/api.py index 495eef490..16de39fea 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -51,5 +51,5 @@ def factory(type): return ArrowFactory(type) -__all__ = ['get', 'utcnow', 'now', 'factory', 'iso'] +__all__ = ['get', 'utcnow', 'now', 'factory'] diff --git a/arrow/arrow.py b/arrow/arrow.py index b17c435cc..256d2e398 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -598,6 +598,7 @@ def humanize(self, other=None, locale='en_us', only_distance=False): Defaults to now in the current :class:`Arrow ` object's timezone. :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'. :param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part. + Usage:: >>> earlier = arrow.utcnow().replace(hours=-2) diff --git a/arrow/locales.py b/arrow/locales.py index 5af426746..9a68d2dc9 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -7,8 +7,8 @@ def get_locale(name): - '''Returns an appropriate :class:`Locale ` corresponding - to an inpute locale name. + '''Returns an appropriate :class:`Locale ` + corresponding to an inpute locale name. :param name: the name of the locale. diff --git a/docs/conf.py b/docs/conf.py index 92fa464ee..95305aab7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -64,7 +64,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ['_build', '_themes'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None @@ -123,7 +123,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. diff --git a/docs/index.rst b/docs/index.rst index a4d2475bb..976abc933 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -205,7 +205,7 @@ Call datetime functions that return properties: Replace & shift =============== -Get a new :class:`Arrow ` object, with altered attributes, just as you would with a datetime: +Get a new :class:`Arrow ` object, with altered attributes, just as you would with a datetime: .. code-block:: python From 66eec78128adde75a65b0f0167bc6c691987a45a Mon Sep 17 00:00:00 2001 From: Buck Evan Date: Wed, 16 Nov 2016 16:42:56 -0800 Subject: [PATCH 063/649] implement, test, and document half-even rounding for fractional seconds --- arrow/parser.py | 12 +++++++++++- docs/index.rst | 11 ++++------- tests/parser_tests.py | 12 ++++-------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 0805f5ce3..69ed5acb9 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -200,7 +200,17 @@ def _parse_token(self, token, value, parts): # We want the six most significant digits as an integer, rounded. # FIXME: add nanosecond support somehow? value = value.ljust(7, str('0')) - parts['microsecond'] = int(value[:6]) + + # floating-point (IEEE-754) defaults to half-to-even rounding + seventh_digit = int(value[6]) + if seventh_digit == 5: + rounding = int(value[5]) % 2 + elif seventh_digit > 5: + rounding = 1 + else: + rounding = 0 + + parts['microsecond'] = int(value[:6]) + rounding elif token == 'X': parts['timestamp'] = int(value) diff --git a/docs/index.rst b/docs/index.rst index 976abc933..d579803b0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -429,13 +429,9 @@ Use the following tokens in parsing and formatting. Note that they're not the s +--------------------------------+--------------+-------------------------------------------+ | |s |0, 1, 2 ... 58, 59 | +--------------------------------+--------------+-------------------------------------------+ -|**Sub-second** |SSS |000, 001, 002 ... 998, 999 | +|**Sub-second** |S... |0, 02, 003, 000006, 123123123123... [#t3]_ | +--------------------------------+--------------+-------------------------------------------+ -| |SS |00, 01, 02 ... 98, 99 | -+--------------------------------+--------------+-------------------------------------------+ -| |S |0, 1, 2 ... 8, 9 | -+--------------------------------+--------------+-------------------------------------------+ -|**Timezone** |ZZZ |Asia/Baku, Europe/Warsaw, GMT ... [#t3]_ | +|**Timezone** |ZZZ |Asia/Baku, Europe/Warsaw, GMT ... [#t4]_ | +--------------------------------+--------------+-------------------------------------------+ | |ZZ |-07:00, -06:00 ... +06:00, +07:00 | +--------------------------------+--------------+-------------------------------------------+ @@ -448,7 +444,8 @@ Use the following tokens in parsing and formatting. Note that they're not the s .. [#t1] localization support for parsing and formatting .. [#t2] localization support only for formatting -.. [#t3] timezone names from `tz database `_ provided via dateutil package +.. [#t3] the result is truncated to microseconds, with `half-to-even rounding `_. +.. [#t4] timezone names from `tz database `_ provided via dateutil package --------- API Guide diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 1dd7d64ea..67cf32f79 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -199,31 +199,27 @@ def test_parse_subsecond(self): assertEqual(self.parser.parse_iso('2013-01-01 12:30:45.987654'), expected) def test_parse_subsecond_rounding(self): - """currently, we've decided there's no rounding""" + expected = datetime(2013, 1, 1, 12, 30, 45, 987654) format = 'YYYY-MM-DD HH:mm:ss.S' # round up string = '2013-01-01 12:30:45.9876539' - expected = datetime(2013, 1, 1, 12, 30, 45, 987653) assertEqual(self.parser.parse(string, format), expected) assertEqual(self.parser.parse_iso(string), expected) # round down string = '2013-01-01 12:30:45.98765432' - expected = datetime(2013, 1, 1, 12, 30, 45, 987654) assertEqual(self.parser.parse(string, format), expected) #import pudb; pudb.set_trace() assertEqual(self.parser.parse_iso(string), expected) # round half-up string = '2013-01-01 12:30:45.987653521' - expected = datetime(2013, 1, 1, 12, 30, 45, 987653) assertEqual(self.parser.parse(string, format), expected) assertEqual(self.parser.parse_iso(string), expected) # round half-down string = '2013-01-01 12:30:45.9876545210' - expected = datetime(2013, 1, 1, 12, 30, 45, 987654) assertEqual(self.parser.parse(string, format), expected) assertEqual(self.parser.parse_iso(string), expected) @@ -431,13 +427,13 @@ def test_YYYY_MM_DDTHH_mm_ss_S(self): # is the preferred sign. assertEqual( self.parser.parse_iso('2013-02-03T04:05:06,789123678'), - datetime(2013, 2, 3, 4, 5, 6, 789123) + datetime(2013, 2, 3, 4, 5, 6, 789124) ) # there is no limit on the number of decimal places assertEqual( self.parser.parse_iso('2013-02-03T04:05:06.789123678'), - datetime(2013, 2, 3, 4, 5, 6, 789123) + datetime(2013, 2, 3, 4, 5, 6, 789124) ) def test_YYYY_MM_DDTHH_mm_ss_SZ(self): @@ -490,7 +486,7 @@ def test_gnu_date(self): assertEqual( self.parser.parse_iso('2016-11-16 09:51:14.682141526-08:00'), datetime( - 2016, 11, 16, 9, 51, 14, 682141, + 2016, 11, 16, 9, 51, 14, 682142, tzinfo=tz.tzoffset(None, -3600 * 8), ) ) From ac7fd37f5f51ef22e2502a1650ef7c9f1594ab61 Mon Sep 17 00:00:00 2001 From: Joseph Kahn Date: Wed, 16 Nov 2016 21:15:34 -0500 Subject: [PATCH 064/649] test test_get_iteration_params respects limit --- tests/arrow_tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 7735b20e6..023ae9221 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1151,6 +1151,7 @@ def test_get_iteration_params(self): assertEqual(arrow.Arrow._get_iteration_params('end', None), ('end', sys.maxsize)) assertEqual(arrow.Arrow._get_iteration_params(None, 100), (arrow.Arrow.max, 100)) + assertEqual(arrow.Arrow._get_iteration_params(100, 120), (100, 120)) with assertRaises(Exception): arrow.Arrow._get_iteration_params(None, None) From 0e2bb033bb7f33161394adfeeee5f6a96559828f Mon Sep 17 00:00:00 2001 From: Nikita Voronchev Date: Mon, 21 Nov 2016 12:58:43 +0300 Subject: [PATCH 065/649] Update arrow.py Fix `relativedelta` subtraction from `arrow` --- arrow/arrow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index b17c435cc..3b38358c3 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -688,7 +688,7 @@ def __radd__(self, other): def __sub__(self, other): - if isinstance(other, timedelta): + if isinstance(other, (timedelta, relativedelta)): return self.fromdatetime(self._datetime - other, self._datetime.tzinfo) elif isinstance(other, datetime): From 07d63083fd40e52b5daee92eae3d3983a9e3ef75 Mon Sep 17 00:00:00 2001 From: Andrew Elkins Date: Sat, 26 Nov 2016 19:09:39 -0800 Subject: [PATCH 066/649] Update version --- arrow/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/__init__.py b/arrow/__init__.py index 916297a40..097d590d9 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -4,5 +4,5 @@ from .factory import ArrowFactory from .api import get, now, utcnow -__version__ = '0.8.0' +__version__ = '0.9.0' VERSION = __version__ From fa5bce04765198071c17fa2ee889c16b5dbe6377 Mon Sep 17 00:00:00 2001 From: Michael Watson Date: Mon, 28 Nov 2016 14:46:38 +0000 Subject: [PATCH 067/649] Fix negative offset for UTC Specifying ZZ in a format string gives an offest of -00:00 for UTC, despite the arrow object repr showing +00:00. Detect `using >= 0` for consistency. --- arrow/formatter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/formatter.py b/arrow/formatter.py index 0ae23895e..50fd3a179 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -94,7 +94,7 @@ def _format_token(self, dt, token): tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo total_minutes = int(util.total_seconds(tz.utcoffset(dt)) / 60) - sign = '+' if total_minutes > 0 else '-' + sign = '+' if total_minutes >= 0 else '-' total_minutes = abs(total_minutes) hour, minute = divmod(total_minutes, 60) From 2deb0a8914ebbfcdb79422d767c8a68956a82107 Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Mon, 28 Nov 2016 09:57:48 -0500 Subject: [PATCH 068/649] Tweak docs after merging with the replace/shift split --- arrow/arrow.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 90da57961..40af807c5 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -408,7 +408,7 @@ def replace(self, **kwargs): ''' Returns a new :class:`Arrow ` object with attributes updated according to inputs. - Use single property names to set their value absolutely:: + Use property names to set their value absolutely:: >>> import arrow >>> arw = arrow.utcnow() @@ -417,11 +417,6 @@ def replace(self, **kwargs): >>> arw.replace(year=2014, month=6) - Use plural property names to shift their current value relatively:: - - >>> arw.replace(years=1, months=-1) - - You can also replace the timezone without conversion, using a :ref:`timezone expression `:: @@ -467,7 +462,7 @@ def shift(self, **kwargs): ''' Returns a new :class:`Arrow ` object with attributes updated according to inputs. - Use plural property names to shift their current value relatively: + Use pluralized property names to shift their current value relatively: >>> import arrow >>> arw = arrow.utcnow() From a6c9108b7114eb450a73d65fd7f70076fc2e3218 Mon Sep 17 00:00:00 2001 From: Michael Watson Date: Tue, 29 Nov 2016 09:04:14 +0000 Subject: [PATCH 069/649] Test for positive UTC offset --- tests/arrow_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 7f88f3ead..bb456bcca 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -129,7 +129,7 @@ def test_bare_format(self): result = self.arrow.format() - assertEqual(result, '2013-02-03 12:30:45-00:00') + assertEqual(result, '2013-02-03 12:30:45+00:00') def test_format_no_format_string(self): From b1fcf1cc88f464d075edfdc23efeac988755929e Mon Sep 17 00:00:00 2001 From: ns-cweber Date: Tue, 29 Nov 2016 10:45:29 -0600 Subject: [PATCH 070/649] Fixes #378 --- arrow/arrow.py | 2 +- tests/arrow_tests.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index aa1277c48..7a8dd5076 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -310,7 +310,7 @@ def __getattr__(self, name): return self.isocalendar()[1] if name == 'quarter': - return int(self.month/self._MONTHS_PER_QUARTER) + 1 + return int((self.month-1)/self._MONTHS_PER_QUARTER) + 1 if not name.startswith('_'): value = getattr(self._datetime, name, None) diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 9ba37b92b..e181227fb 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -162,6 +162,7 @@ def test_getattr_week(self): assertEqual(self.arrow.week, 1) def test_getattr_quarter(self): + # start dates q1 = arrow.Arrow(2013, 1, 1) q2 = arrow.Arrow(2013, 4, 1) q3 = arrow.Arrow(2013, 8, 1) @@ -171,6 +172,16 @@ def test_getattr_quarter(self): assertEqual(q3.quarter, 3) assertEqual(q4.quarter, 4) + # end dates + q1 = arrow.Arrow(2013, 3, 31) + q2 = arrow.Arrow(2013, 6, 30) + q3 = arrow.Arrow(2013, 9, 30) + q4 = arrow.Arrow(2013, 12, 31) + assertEqual(q1.quarter, 1) + assertEqual(q2.quarter, 2) + assertEqual(q3.quarter, 3) + assertEqual(q4.quarter, 4) + def test_getattr_dt_value(self): assertEqual(self.arrow.year, 2013) From 626c8533ac4b278941c4150333d2ecaa9ec07fa1 Mon Sep 17 00:00:00 2001 From: Andrew Elkins Date: Tue, 29 Nov 2016 09:30:48 -0800 Subject: [PATCH 071/649] Update History --- HISTORY.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 66fe44799..e8d4dea43 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,30 @@ ## History +### 0.10.0 + +- [FIX] Fix getattr off by one for quarter +- [FIX] Fix negative offset for UTC +- [FIX] Update arrow.py + +### 0.9.0 + +- [NEW] Remove duplicate code +- [NEW] Support gnu date iso 8601 +- [NEW] Add support for universal wheels +- [NEW] Slovenian locale +- [NEW] Slovak locale +- [NEW] Romanian locale +- [FIX] respect limit even if end is defined range +- [FIX] Separate replace & shift functions +- [NEW] Added tox +- [FIX] Fix supported Python versions in documentation +- [NEW] Azerbaijani locale added, locale issue fixed in Turkish. +- [FIX] Format ParserError's raise message + +### 0.8.0 + +- [] + ### 0.7.1 - [NEW] Esperanto locale (batisteo) From d7cd81a6fa10dfcb4553edcd78969a27625e1689 Mon Sep 17 00:00:00 2001 From: Andrew Elkins Date: Tue, 29 Nov 2016 09:34:19 -0800 Subject: [PATCH 072/649] Update to version 0.10.0 Includes one change to what is returned from ZZ. Thus the version bump --- arrow/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/__init__.py b/arrow/__init__.py index 097d590d9..63dd6be97 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -4,5 +4,5 @@ from .factory import ArrowFactory from .api import get, now, utcnow -__version__ = '0.9.0' +__version__ = '0.10.0' VERSION = __version__ From 2466c293a9fd0465e2c1d022baead8ddd2a433b9 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 29 Nov 2016 19:18:49 -0800 Subject: [PATCH 073/649] bump sphinx 1.2b1->1.3.5 to fix readthedocs build --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9d4e50993..eb3f9838d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ python-dateutil==2.1 nose==1.3.0 nose-cov==1.6 chai==0.4.8 -sphinx==1.2b1 +sphinx==1.3.5 simplejson==3.6.5 From 9b91649cea8f6930b6105bbe99f5c3bced984c16 Mon Sep 17 00:00:00 2001 From: Dillon Dixon Date: Thu, 1 Dec 2016 14:33:22 -0800 Subject: [PATCH 074/649] added format caching to date parser --- arrow/parser.py | 38 +++++++++++++++++++++++++++----------- requirements.txt | 1 + 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index f3ed56cf1..007655f79 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -5,6 +5,12 @@ from datetime import datetime from dateutil import tz import re + +try: + from functools import lru_cache +except ImportError: + from backports.functools_lru_cache import lru_cache + from arrow import locales @@ -50,7 +56,7 @@ class DateTimeParser(object): MARKERS = ['YYYY', 'MM', 'DD'] SEPARATORS = ['-', '/', '.'] - def __init__(self, locale='en_us'): + def __init__(self, locale='en_us', cache_size=0): self.locale = locales.get_locale(locale) self._input_re_map = self._BASE_INPUT_RE_MAP.copy() @@ -62,7 +68,7 @@ def __init__(self, locale='en_us'): 'dddd': self._choice_re(self.locale.day_names[1:], re.IGNORECASE), 'ddd': self._choice_re(self.locale.day_abbreviations[1:], re.IGNORECASE), - 'd' : re.compile("[1-7]"), + 'd': re.compile(r"[1-7]"), 'a': self._choice_re( (self.locale.meridians['am'], self.locale.meridians['pm']) ), @@ -70,6 +76,9 @@ def __init__(self, locale='en_us'): # ensure backwards compatibility of this token 'A': self._choice_re(self.locale.meridians.values()) }) + if cache_size > 0: + self._generate_pattern_re =\ + lru_cache(maxsize=cache_size)(self._generate_pattern_re) def parse_iso(self, string): @@ -98,8 +107,8 @@ def parse_iso(self, string): # using various separators: -, /, . l = len(self.MARKERS) formats = [separator.join(self.MARKERS[:l-i]) - for i in range(l) - for separator in self.SEPARATORS] + for i in range(l) + for separator in self.SEPARATORS] if has_time and has_tz: formats = [f + 'Z' for f in formats] @@ -109,10 +118,7 @@ def parse_iso(self, string): return self._parse_multiformat(string, formats) - def parse(self, string, fmt): - - if isinstance(fmt, list): - return self._parse_multiformat(string, fmt) + def _generate_pattern_re(self, fmt): # fmt is a string of tokens like 'YYYY-MM-DD' # we construct a new string by replacing each @@ -154,11 +160,21 @@ def parse(self, string, fmt): if i < len(b): final_fmt_pattern += b[i][1:-1] - match = re.search(final_fmt_pattern, string, flags=re.IGNORECASE) + return tokens, re.compile(final_fmt_pattern, flags=re.IGNORECASE) + + def parse(self, string, fmt): + + if isinstance(fmt, list): + return self._parse_multiformat(string, fmt) + + fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt) + + match = fmt_pattern_re.search(string) if match is None: - raise ParserError('Failed to match \'{0}\' when parsing \'{1}\''.format(final_fmt_pattern, string)) + raise ParserError('Failed to match \'{0}\' when parsing \'{1}\'' + .format(fmt_pattern_re.pattern, string)) parts = {} - for token in tokens: + for token in fmt_tokens: if token == 'Do': value = match.group('value') else: diff --git a/requirements.txt b/requirements.txt index 9d4e50993..f0d1ba61f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ nose-cov==1.6 chai==0.4.8 sphinx==1.2b1 simplejson==3.6.5 +backports.functools_lru_cache==1.2.1 \ No newline at end of file From aa7d08fef430a13a5e45bcf9cd68719498d189e5 Mon Sep 17 00:00:00 2001 From: Dillon Dixon Date: Thu, 1 Dec 2016 15:47:35 -0800 Subject: [PATCH 075/649] added tests --- arrow/parser.py | 2 +- tests/parser_tests.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/arrow/parser.py b/arrow/parser.py index 007655f79..193016e49 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -128,7 +128,7 @@ def _generate_pattern_re(self, fmt): offset = 0 # Extract the bracketed expressions to be reinserted later. - escaped_fmt = re.sub(self._ESCAPE_RE, "#" , fmt) + escaped_fmt = re.sub(self._ESCAPE_RE, "#", fmt) # Any number of S is the same as one. escaped_fmt = re.sub('S+', 'S', escaped_fmt) escaped_data = re.findall(self._ESCAPE_RE, fmt) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 67cf32f79..5cfcd1c29 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -59,6 +59,45 @@ def test_parse_token_invalid_meridians(self): self.parser._parse_token('a', 'p..m', parts) assertEqual(parts, {}) + def test_parser_no_caching(self): + expect(parser.DateTimeParser._generate_pattern_re).args('fmt_a').times(100) + + self.parser = parser.DateTimeParser(cache_size=0) + + for _ in range(100): + self.parser._generate_pattern_re('fmt_a') + + def test_parser_1_line_caching(self): + + expect(parser.DateTimeParser._generate_pattern_re).args('fmt_a').times(1) + self.parser = parser.DateTimeParser(cache_size=1) + for _ in range(100): + self.parser._generate_pattern_re('fmt_a') + + expect(parser.DateTimeParser._generate_pattern_re).args('fmt_b').times(1) + for _ in range(100): + self.parser._generate_pattern_re('fmt_a') + self.parser._generate_pattern_re('fmt_b') + + expect(parser.DateTimeParser._generate_pattern_re).args('fmt_a').times(1) + for _ in range(100): + self.parser._generate_pattern_re('fmt_a') + + def test_parser_multiple_line_caching(self): + + expect(parser.DateTimeParser._generate_pattern_re).args('fmt_a').times(1) + self.parser = parser.DateTimeParser(cache_size=2) + for _ in range(100): + self.parser._generate_pattern_re('fmt_a') + + expect(parser.DateTimeParser._generate_pattern_re).args('fmt_b').times(1) + for _ in range(100): + self.parser._generate_pattern_re('fmt_a') + self.parser._generate_pattern_re('fmt_b') + + expect(parser.DateTimeParser._generate_pattern_re).args('fmt_a').times(0) + for _ in range(100): + self.parser._generate_pattern_re('fmt_a') class DateTimeParserParseTests(Chai): From a8f32012170b102613ca8c402efb9f76426ff5f8 Mon Sep 17 00:00:00 2001 From: Dillon Dixon Date: Thu, 1 Dec 2016 16:40:18 -0800 Subject: [PATCH 076/649] fixed tests for Py3 and upgraded chai and python-dateutil --- requirements.txt | 6 +++--- tests/parser_tests.py | 15 +++++++-------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/requirements.txt b/requirements.txt index f0d1ba61f..034e5f64f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -python-dateutil==2.1 +python-dateutil==2.6.0 nose==1.3.0 nose-cov==1.6 -chai==0.4.8 +chai==1.1.1 sphinx==1.2b1 simplejson==3.6.5 -backports.functools_lru_cache==1.2.1 \ No newline at end of file +backports.functools_lru_cache==1.2.1 diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 5cfcd1c29..7682df8c3 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -60,42 +60,41 @@ def test_parse_token_invalid_meridians(self): assertEqual(parts, {}) def test_parser_no_caching(self): - expect(parser.DateTimeParser._generate_pattern_re).args('fmt_a').times(100) + expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_a').times(100) self.parser = parser.DateTimeParser(cache_size=0) - for _ in range(100): self.parser._generate_pattern_re('fmt_a') def test_parser_1_line_caching(self): - expect(parser.DateTimeParser._generate_pattern_re).args('fmt_a').times(1) + expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_a').times(1) self.parser = parser.DateTimeParser(cache_size=1) for _ in range(100): self.parser._generate_pattern_re('fmt_a') - expect(parser.DateTimeParser._generate_pattern_re).args('fmt_b').times(1) + expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_b').times(1) for _ in range(100): self.parser._generate_pattern_re('fmt_a') self.parser._generate_pattern_re('fmt_b') - expect(parser.DateTimeParser._generate_pattern_re).args('fmt_a').times(1) + expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_a').times(1) for _ in range(100): self.parser._generate_pattern_re('fmt_a') def test_parser_multiple_line_caching(self): - expect(parser.DateTimeParser._generate_pattern_re).args('fmt_a').times(1) + expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_a').times(1) self.parser = parser.DateTimeParser(cache_size=2) for _ in range(100): self.parser._generate_pattern_re('fmt_a') - expect(parser.DateTimeParser._generate_pattern_re).args('fmt_b').times(1) + expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_b').times(1) for _ in range(100): self.parser._generate_pattern_re('fmt_a') self.parser._generate_pattern_re('fmt_b') - expect(parser.DateTimeParser._generate_pattern_re).args('fmt_a').times(0) + expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_a').times(0) for _ in range(100): self.parser._generate_pattern_re('fmt_a') From 8c0f58e29b1342180230006fe5a6712aad35f279 Mon Sep 17 00:00:00 2001 From: Dillon Dixon Date: Thu, 1 Dec 2016 17:01:19 -0800 Subject: [PATCH 077/649] fixed coverage to ignore Py2 only imports --- arrow/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 193016e49..e4f368ad5 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -8,8 +8,8 @@ try: from functools import lru_cache -except ImportError: - from backports.functools_lru_cache import lru_cache +except ImportError: # pragma: no cover + from backports.functools_lru_cache import lru_cache # pragma: no cover from arrow import locales From b2bae227ab974e096374ab3e9cef8cbc9635ecfb Mon Sep 17 00:00:00 2001 From: Woo Joong Kim Date: Sun, 4 Dec 2016 22:55:21 -0500 Subject: [PATCH 078/649] implementation of interval function --- arrow/arrow.py | 43 +++++++++++++++++++++++++++++++++++++++++++ tests/arrow_tests.py | 17 +++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/arrow/arrow.py b/arrow/arrow.py index cda55365d..89f7890ac 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -312,6 +312,49 @@ def span_range(cls, frame, start, end, tz=None, limit=None): _range = cls.range(frame, start, end, tz, limit) return [r.span(frame) for r in _range] + @classmethod + def interval(cls, frame, start, end, interval=1, tz=None): + ''' Returns an array of tuples, each :class:`Arrow ` objects, + representing a series of intervals between two inputs. + + :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + :param start: A datetime expression, the start of the range. + :param end: (optional) A datetime expression, the end of the range. + :param interval: (optional) Time interval for the given time frame. + :param tz: (optional) A timezone expression. Defaults to UTC. + + Supported frame values: year, quarter, month, week, day, hour, minute, second + + Recognized datetime expressions: + + - An :class:`Arrow ` object. + - A ``datetime`` object. + + Recognized timezone expressions: + + - A ``tzinfo`` object. + - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. + - A ``str`` in ISO-8601 style, as in '+07:00'. + - A ``str``, one of the following: 'local', 'utc', 'UTC'. + + Usage: + + >>> start = datetime(2013, 5, 5, 12, 30) + >>> end = datetime(2013, 5, 5, 17, 15) + >>> for r in arrow.Arrow.interval('hour', start, end, 2): + ... print r + ... + (, ) + (, ) + (, ) + ''' + if interval < 1: + raise ValueError("interval has to be a positive integer") + + spanRange = cls.span_range(frame,start,end,tz) + + bound = (len(spanRange) // interval) * interval + return [ (spanRange[i][0],spanRange[i+ interval - 1][1]) for i in range(0,bound, interval) ] # representations diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 65022f945..6cf6ddde6 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -923,6 +923,23 @@ def test_aware_tz(self): assertEqual(f.tzinfo, tz.gettz('US/Central')) assertEqual(c.tzinfo, tz.gettz('US/Central')) +class ArrowIntervalTests(Chai): + + def test_incorrect_input(self): + correct = True + try: + result = arrow.Arrow.interval('month', datetime(2013, 1, 2), datetime(2013, 4, 15),0) + except: + correct = False + + assertEqual(correct,False) + + def test_correct(self): + result = arrow.Arrow.interval('hour', datetime(2013, 5, 5, 12, 30), datetime(2013, 5, 5, 17, 15),2) + + assertEqual(result,[(arrow.Arrow(2013, 5, 5, 12), arrow.Arrow(2013, 5, 5, 13, 59, 59, 999999)), + (arrow.Arrow(2013, 5, 5, 14), arrow.Arrow(2013, 5, 5, 15, 59, 59, 999999)), + (arrow.Arrow(2013, 5, 5, 16), arrow.Arrow(2013, 5, 5, 17, 59, 59, 999999))]) class ArrowSpanTests(Chai): From d9498202912c27fcffcf20269aa92f1f4472ac80 Mon Sep 17 00:00:00 2001 From: James Cook Date: Wed, 7 Dec 2016 02:14:09 -0500 Subject: [PATCH 079/649] Implement weekday shifting for #64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I’m not terribly happy with the implementation - I don’t grok why dateutil handles some values (negatives and zeroes) the way it does, but I figure this is good to generate discussion anyway. --- arrow/arrow.py | 17 +++++++++++- tests/arrow_tests.py | 64 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index cda55365d..b2e5c6a6e 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -471,13 +471,28 @@ def shift(self, **kwargs): >>> arw.shift(years=1, months=-1) + Day-of-the-week relative shifting can use either Python's weekday numbers + (Monday = 0, Tuesday = 1 .. Sunday = 6) or using dateutil.relativedelta's + day instances (MO, TU .. SU). When using weekday numbers, the returned + date will always be greater than or equal to the starting date. + + Using the above code (which is a Saturday) and asking it to shift to Saturday: + + >>> arw.shift(weekday=5) + + + While asking for a Monday: + + >>> arw.shift(weekday=0) + + ''' relative_kwargs = {} for key, value in kwargs.items(): - if key in self._ATTRS_PLURAL or key in ['weeks', 'quarters']: + if key in self._ATTRS_PLURAL or key in ['weeks', 'quarters', 'weekday']: relative_kwargs[key] = value else: raise AttributeError() diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 65022f945..a4ddea279 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -6,6 +6,7 @@ from datetime import date, datetime, timedelta from dateutil import tz +from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU import simplejson as json import warnings import calendar @@ -592,6 +593,48 @@ def test_shift(self): assertEqual(arw.shift(seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) assertEqual(arw.shift(microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1)) + # Remember: Python's weekday 0 is Monday + assertEqual(arw.shift(weekday=0), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + assertEqual(arw.shift(weekday=1), arrow.Arrow(2013, 5, 7, 12, 30, 45)) + assertEqual(arw.shift(weekday=2), arrow.Arrow(2013, 5, 8, 12, 30, 45)) + assertEqual(arw.shift(weekday=3), arrow.Arrow(2013, 5, 9, 12, 30, 45)) + assertEqual(arw.shift(weekday=4), arrow.Arrow(2013, 5, 10, 12, 30, 45)) + assertEqual(arw.shift(weekday=5), arrow.Arrow(2013, 5, 11, 12, 30, 45)) + assertEqual(arw.shift(weekday=6), arw) + + with assertRaises(IndexError): + arw.shift(weekday=7) + + # Use dateutil.relativedelta's convenient day instances + assertEqual(arw.shift(weekday=MO), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + assertEqual(arw.shift(weekday=MO(0)), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + assertEqual(arw.shift(weekday=MO(1)), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + assertEqual(arw.shift(weekday=MO(2)), arrow.Arrow(2013, 5, 13, 12, 30, 45)) + assertEqual(arw.shift(weekday=TU), arrow.Arrow(2013, 5, 7, 12, 30, 45)) + assertEqual(arw.shift(weekday=TU(0)), arrow.Arrow(2013, 5, 7, 12, 30, 45)) + assertEqual(arw.shift(weekday=TU(1)), arrow.Arrow(2013, 5, 7, 12, 30, 45)) + assertEqual(arw.shift(weekday=TU(2)), arrow.Arrow(2013, 5, 14, 12, 30, 45)) + assertEqual(arw.shift(weekday=WE), arrow.Arrow(2013, 5, 8, 12, 30, 45)) + assertEqual(arw.shift(weekday=WE(0)), arrow.Arrow(2013, 5, 8, 12, 30, 45)) + assertEqual(arw.shift(weekday=WE(1)), arrow.Arrow(2013, 5, 8, 12, 30, 45)) + assertEqual(arw.shift(weekday=WE(2)), arrow.Arrow(2013, 5, 15, 12, 30, 45)) + assertEqual(arw.shift(weekday=TH), arrow.Arrow(2013, 5, 9, 12, 30, 45)) + assertEqual(arw.shift(weekday=TH(0)), arrow.Arrow(2013, 5, 9, 12, 30, 45)) + assertEqual(arw.shift(weekday=TH(1)), arrow.Arrow(2013, 5, 9, 12, 30, 45)) + assertEqual(arw.shift(weekday=TH(2)), arrow.Arrow(2013, 5, 16, 12, 30, 45)) + assertEqual(arw.shift(weekday=FR), arrow.Arrow(2013, 5, 10, 12, 30, 45)) + assertEqual(arw.shift(weekday=FR(0)), arrow.Arrow(2013, 5, 10, 12, 30, 45)) + assertEqual(arw.shift(weekday=FR(1)), arrow.Arrow(2013, 5, 10, 12, 30, 45)) + assertEqual(arw.shift(weekday=FR(2)), arrow.Arrow(2013, 5, 17, 12, 30, 45)) + assertEqual(arw.shift(weekday=SA), arrow.Arrow(2013, 5, 11, 12, 30, 45)) + assertEqual(arw.shift(weekday=SA(0)), arrow.Arrow(2013, 5, 11, 12, 30, 45)) + assertEqual(arw.shift(weekday=SA(1)), arrow.Arrow(2013, 5, 11, 12, 30, 45)) + assertEqual(arw.shift(weekday=SA(2)), arrow.Arrow(2013, 5, 18, 12, 30, 45)) + assertEqual(arw.shift(weekday=SU), arw) + assertEqual(arw.shift(weekday=SU(0)), arw) + assertEqual(arw.shift(weekday=SU(1)), arw) + assertEqual(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) @@ -607,6 +650,27 @@ def test_shift_negative(self): assertEqual(arw.shift(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) assertEqual(arw.shift(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999)) + # Not sure how practical these negative weekdays are + assertEqual(arw.shift(weekday=-1), arw.shift(weekday=SU)) + assertEqual(arw.shift(weekday=-2), arw.shift(weekday=SA)) + assertEqual(arw.shift(weekday=-3), arw.shift(weekday=FR)) + assertEqual(arw.shift(weekday=-4), arw.shift(weekday=TH)) + assertEqual(arw.shift(weekday=-5), arw.shift(weekday=WE)) + assertEqual(arw.shift(weekday=-6), arw.shift(weekday=TU)) + assertEqual(arw.shift(weekday=-7), arw.shift(weekday=MO)) + + with assertRaises(IndexError): + arw.shift(weekday=-8) + + assertEqual(arw.shift(weekday=MO(-1)), arrow.Arrow(2013, 4, 29, 12, 30, 45)) + assertEqual(arw.shift(weekday=TU(-1)), arrow.Arrow(2013, 4, 30, 12, 30, 45)) + assertEqual(arw.shift(weekday=WE(-1)), arrow.Arrow(2013, 5, 1, 12, 30, 45)) + assertEqual(arw.shift(weekday=TH(-1)), arrow.Arrow(2013, 5, 2, 12, 30, 45)) + assertEqual(arw.shift(weekday=FR(-1)), arrow.Arrow(2013, 5, 3, 12, 30, 45)) + assertEqual(arw.shift(weekday=SA(-1)), arrow.Arrow(2013, 5, 4, 12, 30, 45)) + assertEqual(arw.shift(weekday=SU(-1)), arw) + assertEqual(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) From bfbd4a00cace83bc441d34c24032d4ce50d41710 Mon Sep 17 00:00:00 2001 From: Brutus Date: Sun, 18 Dec 2016 16:41:09 +0100 Subject: [PATCH 080/649] Update locales.py `AustriaLocale` no longer shadows `GermanLocale`. --- arrow/locales.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 7cf7f4c34..6ee9943f9 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -839,13 +839,10 @@ class GermanLocale(_DeutschLocaleCommonMixin, Locale): names = ['de', 'de_de'] - timeframes = _DeutschLocaleCommonMixin.timeframes.copy() - timeframes['days'] = '{0} Tagen' - class AustriaLocale(_DeutschLocaleCommonMixin, Locale): - names = ['de', 'de_at'] + names = ['de_at'] timeframes = _DeutschLocaleCommonMixin.timeframes.copy() timeframes['days'] = '{0} Tage' From a42770b94a7e0231fa3efc9c9a069d0da8cfc325 Mon Sep 17 00:00:00 2001 From: "tanakiat.srisaranyakul" Date: Tue, 20 Dec 2016 15:48:24 +0700 Subject: [PATCH 081/649] fix typo in month full name in Thai Language --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index 7cf7f4c34..c65a07225 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1758,7 +1758,7 @@ class ThaiLocale(Locale): } month_names = ['', 'มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', - 'พฤษภาคม', 'มิถุนายน', 'กรกฏาคม', 'สิงหาคม', + 'พฤษภาคม', 'มิถุนายน', 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'] month_abbreviations = ['', 'ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', From 7f69ace2d3465796f130e89ec3318fedb2c55a00 Mon Sep 17 00:00:00 2001 From: Andrew Elkins Date: Fri, 30 Dec 2016 21:42:08 -0800 Subject: [PATCH 082/649] Fix for poperly handle tz passed in form pytz Previously: arrow.Arrow.fromtimestamp(0, pytz.timezone('Europe/Paris')) # Now: arrow.Arrow.fromtimestamp(0, pytz.timezone('Europe/Paris')) # --- arrow/arrow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 89f7890ac..a1d7ed5ed 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -116,7 +116,7 @@ def fromtimestamp(cls, timestamp, tzinfo=None): dt = datetime.fromtimestamp(timestamp, tzinfo) return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, tzinfo) + dt.microsecond, dt.tzinfo) @classmethod def utcfromtimestamp(cls, timestamp): From a20b22b410b52e456580db37b1431119c2ed7df8 Mon Sep 17 00:00:00 2001 From: galaplexus Date: Sat, 31 Dec 2016 17:08:50 -0500 Subject: [PATCH 083/649] Humanizegranularity (#388) * Main functionnalities * New feature : Granularity with time * Included decriptive error with tests * Corrected a bug Arrow don't supports week granularity --- arrow/arrow.py | 110 ++++++++++++++++++++++++++----------------- arrow/locales.py | 4 +- tests/arrow_tests.py | 54 +++++++++++++++++++++ 3 files changed, 123 insertions(+), 45 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index e974613f1..440dcab11 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -10,6 +10,7 @@ from datetime import datetime, timedelta, tzinfo from dateutil import tz as dateutil_tz from dateutil.relativedelta import relativedelta +from math import trunc import calendar import sys import warnings @@ -699,14 +700,14 @@ def format(self, fmt='YYYY-MM-DD HH:mm:ssZZ', locale='en_us'): return formatter.DateTimeFormatter(locale).format(self._datetime, fmt) - def humanize(self, other=None, locale='en_us', only_distance=False): + def humanize(self, other=None, locale='en_us', only_distance=False, granularity='auto'): ''' Returns a localized, humanized representation of a relative difference in time. :param other: (optional) an :class:`Arrow ` or ``datetime`` object. Defaults to now in the current :class:`Arrow ` object's timezone. :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'. :param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part. - + :param granularity: (optinal) defines the precision of the output. Set it to strings 'second', 'minute', 'hour', 'day', 'month' or 'year'. Usage:: >>> earlier = arrow.utcnow().replace(hours=-2) @@ -741,48 +742,71 @@ def humanize(self, other=None, locale='en_us', only_distance=False): sign = -1 if delta < 0 else 1 diff = abs(delta) delta = diff + + if granularity=='auto': + if diff < 10: + return locale.describe('now', only_distance=only_distance) + + if diff < 45: + seconds = sign * delta + return locale.describe('seconds', seconds, only_distance=only_distance) + + elif diff < 90: + return locale.describe('minute', sign, only_distance=only_distance) + elif diff < 2700: + minutes = sign * int(max(delta / 60, 2)) + return locale.describe('minutes', minutes, only_distance=only_distance) + + elif diff < 5400: + return locale.describe('hour', sign, only_distance=only_distance) + elif diff < 79200: + hours = sign * int(max(delta / 3600, 2)) + return locale.describe('hours', hours, only_distance=only_distance) + + elif diff < 129600: + return locale.describe('day', sign, only_distance=only_distance) + elif diff < 2160000: + days = sign * int(max(delta / 86400, 2)) + return locale.describe('days', days, only_distance=only_distance) + + elif diff < 3888000: + return locale.describe('month', sign, only_distance=only_distance) + elif diff < 29808000: + self_months = self._datetime.year * 12 + self._datetime.month + other_months = dt.year * 12 + dt.month + + months = sign * int(max(abs(other_months - self_months), 2)) + + return locale.describe('months', months, only_distance=only_distance) + + elif diff < 47260800: + return locale.describe('year', sign, only_distance=only_distance) + else: + years = sign * int(max(delta / 31536000, 2)) + return locale.describe('years', years, only_distance=only_distance) - if diff < 10: - return locale.describe('now', only_distance=only_distance) - - if diff < 45: - return locale.describe('seconds', sign, only_distance=only_distance) - - elif diff < 90: - return locale.describe('minute', sign, only_distance=only_distance) - elif diff < 2700: - minutes = sign * int(max(delta / 60, 2)) - return locale.describe('minutes', minutes, only_distance=only_distance) - - elif diff < 5400: - return locale.describe('hour', sign, only_distance=only_distance) - elif diff < 79200: - hours = sign * int(max(delta / 3600, 2)) - return locale.describe('hours', hours, only_distance=only_distance) - - elif diff < 129600: - return locale.describe('day', sign, only_distance=only_distance) - elif diff < 2160000: - days = sign * int(max(delta / 86400, 2)) - return locale.describe('days', days, only_distance=only_distance) - - elif diff < 3888000: - return locale.describe('month', sign, only_distance=only_distance) - elif diff < 29808000: - self_months = self._datetime.year * 12 + self._datetime.month - other_months = dt.year * 12 + dt.month - - months = sign * int(max(abs(other_months - self_months), 2)) - - return locale.describe('months', months, only_distance=only_distance) - - elif diff < 47260800: - return locale.describe('year', sign, only_distance=only_distance) else: - years = sign * int(max(delta / 31536000, 2)) - return locale.describe('years', years, only_distance=only_distance) - - + if granularity == 'second': + delta = sign * delta + if(abs(delta) < 2): + return locale.describe('now', only_distance=only_distance) + elif granularity == 'minute': + delta = sign * delta / float(60) + elif granularity == 'hour': + delta = sign * delta / float(60*60) + elif granularity == 'day': + delta = sign * delta / float(60*60*24) + elif granularity == 'month': + delta = sign * delta / float(60*60*24*30.5) + elif granularity == 'year': + delta = sign * delta / float(60*60*24*365.25) + else: + raise AttributeError('Error. Could not understand your level of granularity. Please select between \ + "second", "minute", "hour", "day", "week", "month" or "year"') + + if(trunc(abs(delta)) != 1): + granularity += 's' + return locale.describe(granularity, delta, only_distance=False) # math def __add__(self, other): @@ -805,7 +829,7 @@ def __sub__(self, other): elif isinstance(other, Arrow): return self._datetime - other._datetime - + # print granularity raise TypeError() def __rsub__(self, other): diff --git a/arrow/locales.py b/arrow/locales.py index 96b12f3f5..db36114ba 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -4,6 +4,7 @@ import inspect import sys +from math import trunc def get_locale(name): @@ -169,8 +170,7 @@ def _name_to_ordinal(self, lst): return dict(map(lambda i: (i[1].lower(), i[0] + 1), enumerate(lst[1:]))) def _format_timeframe(self, timeframe, delta): - - return self.timeframes[timeframe].format(abs(delta)) + return self.timeframes[timeframe].format(trunc(abs(delta))) def _format_relative(self, humanized, timeframe, delta): diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 5f66e44a4..e5a8b5223 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1115,6 +1115,60 @@ def setUp(self): self.datetime = datetime(2013, 1, 1) self.now = arrow.Arrow.utcnow() + def test_granularity(self): + + assertEqual(self.now.humanize(granularity = 'second'), 'just now') + + later1 = self.now.shift(seconds=1) + assertEqual(self.now.humanize(later1, granularity = 'second'), 'just now') + assertEqual(later1.humanize(self.now, granularity = 'second'), 'just now') + assertEqual(self.now.humanize(later1, granularity = 'minute'), '0 minutes ago') + assertEqual(later1.humanize(self.now, granularity = 'minute'), 'in 0 minutes') + + later100 = self.now.shift(seconds=100) + assertEqual(self.now.humanize(later100, granularity = 'second'), 'seconds ago') + assertEqual(later100.humanize(self.now, granularity = 'second'), 'in seconds') + assertEqual(self.now.humanize(later100, granularity = 'minute'), 'a minute ago') + assertEqual(later100.humanize(self.now, granularity = 'minute'), 'in a minute') + assertEqual(self.now.humanize(later100, granularity = 'hour'), '0 hours ago') + assertEqual(later100.humanize(self.now, granularity = 'hour'), 'in 0 hours') + + later4000 = self.now.shift(seconds=4000) + assertEqual(self.now.humanize(later4000, granularity = 'minute'), '66 minutes ago') + assertEqual(later4000.humanize(self.now, granularity = 'minute'), 'in 66 minutes') + assertEqual(self.now.humanize(later4000, granularity = 'hour'), 'an hour ago') + assertEqual(later4000.humanize(self.now, granularity = 'hour'), 'in an hour') + assertEqual(self.now.humanize(later4000, granularity = 'day'), '0 days ago') + assertEqual(later4000.humanize(self.now, granularity = 'day'), 'in 0 days') + + later105 = self.now.shift(seconds=10 ** 5) + assertEqual(self.now.humanize(later105, granularity = 'hour'), '27 hours ago') + assertEqual(later105.humanize(self.now, granularity = 'hour'), 'in 27 hours') + assertEqual(self.now.humanize(later105, granularity = 'day'), 'a day ago') + assertEqual(later105.humanize(self.now, granularity = 'day'), 'in a day') + assertEqual(self.now.humanize(later105, granularity = 'month'), '0 months ago') + assertEqual(later105.humanize(self.now, granularity = 'month'), 'in 0 months') + + later106 = self.now.shift(seconds=3 * 10 ** 6) + assertEqual(self.now.humanize(later106, granularity = 'day'), '34 days ago') + assertEqual(later106.humanize(self.now, granularity = 'day'), 'in 34 days') + assertEqual(self.now.humanize(later106, granularity = 'month'), 'a month ago') + assertEqual(later106.humanize(self.now, granularity = 'month'), 'in a month') + assertEqual(self.now.humanize(later106, granularity = 'year'), '0 years ago') + assertEqual(later106.humanize(self.now, granularity = 'year'), 'in 0 years') + + later506 = self.now.shift(seconds=50 * 10 ** 6) + assertEqual(self.now.humanize(later506, granularity = 'month'), '18 months ago') + assertEqual(later506.humanize(self.now, granularity = 'month'), 'in 18 months') + assertEqual(self.now.humanize(later506, granularity = 'year'), 'a year ago') + assertEqual(later506.humanize(self.now, granularity = 'year'), 'in a year') + + later108 = self.now.shift(seconds=10 ** 8) + assertEqual(self.now.humanize(later108, granularity = 'year'), '3 years ago') + assertEqual(later108.humanize(self.now, granularity = 'year'), 'in 3 years') + with assertRaises(AttributeError): + self.now.humanize(later108, granularity = 'years') + def test_seconds(self): later = self.now.shift(seconds=10) From 24f06c145f9c1f4df590a80a26df2962759f71c0 Mon Sep 17 00:00:00 2001 From: Andrew Elkins Date: Sat, 31 Dec 2016 14:14:45 -0800 Subject: [PATCH 084/649] Remove print statement and fix typo (#404) --- arrow/arrow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 440dcab11..e35ea6ade 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -707,7 +707,7 @@ def humanize(self, other=None, locale='en_us', only_distance=False, granularity= Defaults to now in the current :class:`Arrow ` object's timezone. :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'. :param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part. - :param granularity: (optinal) defines the precision of the output. Set it to strings 'second', 'minute', 'hour', 'day', 'month' or 'year'. + :param granularity: (optional) defines the precision of the output. Set it to strings 'second', 'minute', 'hour', 'day', 'month' or 'year'. Usage:: >>> earlier = arrow.utcnow().replace(hours=-2) @@ -829,7 +829,7 @@ def __sub__(self, other): elif isinstance(other, Arrow): return self._datetime - other._datetime - # print granularity + raise TypeError() def __rsub__(self, other): From cb41c433e9871f01ccfa1ddb7fd83b03ccacc545 Mon Sep 17 00:00:00 2001 From: Andrew Elkins Date: Mon, 2 Jan 2017 11:52:30 -0800 Subject: [PATCH 085/649] Update docs version to 0.10.0 Fixes #279 (#405) --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 95305aab7..33b6de5ac 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.4.4' +version = '0.10.0' # The full version, including alpha/beta/rc tags. -release = '0.4.4' +release = '0.10.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From fb7137a73a096516b439aa9a80b05d727c953e3c Mon Sep 17 00:00:00 2001 From: Andrew Elkins Date: Mon, 2 Jan 2017 11:53:49 -0800 Subject: [PATCH 086/649] Update conf.py --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 33b6de5ac..9313847af 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.10.0' +version = '0.11.0' # The full version, including alpha/beta/rc tags. -release = '0.10.0' +release = '0.11.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 93d699020d45dacd5f2073296a21345a884aac77 Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 8 Jan 2017 08:05:03 +1100 Subject: [PATCH 087/649] Replace ".replace" with new ".shift" function in docs (#406) Commit 15d903a (from #349), separated the replace and shift features into two functions. However the docs still show the old, deprecated and more confusing use of replace to shift the time. --- arrow/arrow.py | 4 ++-- docs/index.rst | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index e35ea6ade..89bd96311 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -710,11 +710,11 @@ def humanize(self, other=None, locale='en_us', only_distance=False, granularity= :param granularity: (optional) defines the precision of the output. Set it to strings 'second', 'minute', 'hour', 'day', 'month' or 'year'. Usage:: - >>> earlier = arrow.utcnow().replace(hours=-2) + >>> earlier = arrow.utcnow().shift(hours=-2) >>> earlier.humanize() '2 hours ago' - >>> later = later = earlier.replace(hours=4) + >>> later = later = earlier.shift(hours=4) >>> later.humanize(earlier) 'in 4 hours' diff --git a/docs/index.rst b/docs/index.rst index 9e2cba4bc..3a2c6f8c1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -53,7 +53,7 @@ Quickstart >>> utc - >>> utc = utc.replace(hours=-1) + >>> utc = utc.shift(hours=-1) >>> utc @@ -220,7 +220,7 @@ Or, get one with attributes shifted forward or backward: .. code-block:: python - >>> arw.replace(weeks=+3) + >>> arw.shift(weeks=+3) Even replace the timezone without altering other attributes: @@ -274,7 +274,7 @@ Humanize relative to now: .. code-block:: python - >>> past = arrow.utcnow().replace(hours=-1) + >>> past = arrow.utcnow().shift(hours=-1) >>> past.humanize() 'an hour ago' @@ -283,7 +283,7 @@ Or another Arrow, or datetime: .. code-block:: python >>> present = arrow.utcnow() - >>> future = present.replace(hours=2) + >>> future = present.shift(hours=2) >>> future.humanize(present) 'in 2 hours' @@ -291,7 +291,7 @@ Support for a growing number of locales (see `locales.py` for supported language .. code-block:: python - >>> future = arrow.utcnow().replace(hours=1) + >>> future = arrow.utcnow().shift(hours=1) >>> future.humanize(a, locale='ru') 'через 2 час(а,ов)' @@ -363,7 +363,7 @@ Use factories to harness Arrow's module API for a custom Arrow-derived type. Fi ... xmas = arrow.Arrow(self.year, 12, 25) ... ... if self > xmas: - ... xmas = xmas.replace(years=1) + ... xmas = xmas.shift(years=1) ... ... return (xmas - self).days From 93f489094282808ec80b720b1782b5f361ef42b6 Mon Sep 17 00:00:00 2001 From: Brutus Date: Sat, 7 Jan 2017 22:07:34 +0100 Subject: [PATCH 088/649] Remove `timeframes['days']` handling from `AustriaLocale` (#408) According to this [German SE answer](http://german.stackexchange.com/a/34107/25165) the special case for `timeframes['days']` in `AustriaLocale` is wrong. --- arrow/locales.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index db36114ba..ee18153f6 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -844,9 +844,6 @@ class AustriaLocale(_DeutschLocaleCommonMixin, Locale): names = ['de_at'] - timeframes = _DeutschLocaleCommonMixin.timeframes.copy() - timeframes['days'] = '{0} Tage' - class NorwegianLocale(Locale): From a78bb2ffde8582d648d54ecc0a6b04d61408e4cb Mon Sep 17 00:00:00 2001 From: Patrick Stegmann Date: Tue, 24 Jan 2017 23:47:51 +0100 Subject: [PATCH 089/649] Added more human-readable error message for invalid frames for range/span (#407) * Added more human-readable error message for invalid frames for range/span * Fixed empty field name for string interpolation in range/span error message --- arrow/arrow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 89bd96311..7cd5dcb1f 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1020,7 +1020,8 @@ def _get_frames(cls, name): elif name in ['quarter', 'quarters']: return 'quarter', 'months', 3 - raise AttributeError() + supported = ', '.join(cls._ATTRS + ['week', 'weeks'] + ['quarter', 'quarters']) + raise AttributeError('range/span over frame {0} not supported. Supported frames: {1}'.format(name, supported)) @classmethod def _get_iteration_params(cls, end, limit): From 56e50285b1d10b0c5e6eb3e4b730d2d02910c27e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Tue, 24 Jan 2017 23:48:10 +0100 Subject: [PATCH 090/649] Fixes a typo in the Swiss German translation (#410) --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index ee18153f6..bd1326dca 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1899,7 +1899,7 @@ class SwissLocale(Locale): 'hour': 'einer Stunde', 'hours': '{0} Stunden', 'day': 'einem Tag', - 'days': '{0} Tage', + 'days': '{0} Tagen', 'month': 'einem Monat', 'months': '{0} Monaten', 'year': 'einem Jahr', From fd723b46a6d84ea75af329fe78da6afaf27dfeba Mon Sep 17 00:00:00 2001 From: Zhiming Wang Date: Tue, 24 Jan 2017 17:48:30 -0500 Subject: [PATCH 091/649] ArrowFactory.get: document kwargs locale and tzinfo (#411) --- arrow/factory.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/arrow/factory.py b/arrow/factory.py index 8fae51fda..6c33bbb26 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -32,6 +32,12 @@ def __init__(self, type=Arrow): def get(self, *args, **kwargs): ''' Returns an :class:`Arrow ` object based on flexible inputs. + :param locale: (optional) a ``str`` specifying a locale for the parser. Defaults to + 'en_us'. + :param tzinfo: (optional) a :ref:`timezone expression ` or tzinfo object. + Replaces the timezone unless using an input form that is explicitly UTC or specifies + the timezone in a positional argument. Defaults to UTC. + Usage:: >>> import arrow From fa8199aaead4d4d57ed9bb302d18345438b7bcc6 Mon Sep 17 00:00:00 2001 From: Samer Atiani Date: Mon, 17 Apr 2017 13:07:00 -0400 Subject: [PATCH 092/649] Fix grammar of ArabicLocale --- arrow/locales.py | 64 +++++++++++++++++++++++++++++++++--------- tests/locales_tests.py | 36 ++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 14 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index bd1326dca..4ba0deafc 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1073,34 +1073,70 @@ class AzerbaijaniLocale(Locale): class ArabicLocale(Locale): - - names = ['ar', 'ar_eg'] + names = ['ar', 'ar_eg', 'ar_jo'] past = 'منذ {0}' future = 'خلال {0}' timeframes = { 'now': 'الآن', - 'seconds': 'ثوان', + 'seconds': { + 'double' : 'ثانيتين', + 'ten' : '{0} ثواني', + 'higher' : '{0} ثانية' + }, 'minute': 'دقيقة', - 'minutes': '{0} دقائق', + 'minutes': { + 'double' : 'دقيقتين', + 'ten' : '{0} دقائق', + 'higher' : '{0} دقيقة' + }, 'hour': 'ساعة', - 'hours': '{0} ساعات', + 'hours': { + 'double' : 'ساعتين', + 'ten' : '{0} ساعات', + 'higher' : '{0} ساعة' + }, 'day': 'يوم', - 'days': '{0} أيام', + 'days': { + 'double' : 'يومين', + 'ten' : '{0} أيام', + 'higher' : '{0} يوم' + }, 'month': 'شهر', - 'months': '{0} شهور', + 'months': { + 'double' : 'شهرين', + 'ten' : '{0} أشهر', + 'higher' : '{0} شهر' + }, 'year': 'سنة', - 'years': '{0} سنوات', + 'years': { + 'double' : 'سنتين', + 'ten' : '{0} سنوات', + 'higher' : '{0} سنة' + }, } - month_names = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', - 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] - month_abbreviations = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', - 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] + month_names = ['', 'كانون الثاني', 'شباط', 'آذار', 'نيسان', 'أيار', 'حزيران', 'تموز', 'آب', + 'أيلول', 'تشرين الأول', 'تشرين الثاني', 'كانون الأول'] + month_abbreviations = ['', 'كانون الثاني', 'شباط', 'آذار', 'نيسان', 'أيار', 'حزيران', 'تموز', 'آب', + 'أيلول', 'تشرين الأول', 'تشرين الثاني', 'كانون الأول'] + + day_names = ['', 'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'] + day_abbreviations = ['', 'إثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت', 'أحد'] + + def _format_timeframe(self, timeframe, delta): + form = self.timeframes[timeframe] + delta = abs(delta) + if isinstance(form, dict): + if delta == 2: + form = form['double'] + elif delta > 2 and delta <= 10: + form = form['ten'] + else: + form = form['higher'] - day_names = ['', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'] - day_abbreviations = ['', 'اثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت', 'أحد'] + return form.format(delta) class IcelandicLocale(Locale): diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 1d06fb957..cca75e4d4 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -553,3 +553,39 @@ def test_relative_timeframes(self): self.assertEqual(self.locale._format_relative("câteva secunde", "seconds", 1), "peste câteva secunde") self.assertEqual(self.locale._format_relative("o zi", "day", -1), "o zi în urmă") self.assertEqual(self.locale._format_relative("o zi", "day", 1), "peste o zi") + +class ArabicLocalesTest(Chai): + def setUp(self): + super(ArabicLocalesTest, self).setUp() + + self.locale = locales.ArabicLocale() + + def test_timeframes(self): + + # single + self.assertEqual(self.locale._format_timeframe('minute', 1), 'دقيقة') + self.assertEqual(self.locale._format_timeframe('hour', 1), 'ساعة') + self.assertEqual(self.locale._format_timeframe('day', 1), 'يوم') + self.assertEqual(self.locale._format_timeframe('month', 1), 'شهر') + self.assertEqual(self.locale._format_timeframe('year', 1), 'سنة') + + # double + self.assertEqual(self.locale._format_timeframe('minutes', 2), 'دقيقتين') + self.assertEqual(self.locale._format_timeframe('hours', 2), 'ساعتين') + self.assertEqual(self.locale._format_timeframe('days', 2), 'يومين') + self.assertEqual(self.locale._format_timeframe('months', 2), 'شهرين') + self.assertEqual(self.locale._format_timeframe('years', 2), 'سنتين') + + # up to ten + self.assertEqual(self.locale._format_timeframe('minutes', 3), '3 دقائق') + self.assertEqual(self.locale._format_timeframe('hours', 4), '4 ساعات') + self.assertEqual(self.locale._format_timeframe('days', 5), '5 أيام') + self.assertEqual(self.locale._format_timeframe('months', 6), '6 أشهر') + self.assertEqual(self.locale._format_timeframe('years', 10), '10 سنوات') + + # more than ten + self.assertEqual(self.locale._format_timeframe('minutes', 11), '11 دقيقة') + self.assertEqual(self.locale._format_timeframe('hours', 19), '19 ساعة') + self.assertEqual(self.locale._format_timeframe('months', 24), '24 شهر') + self.assertEqual(self.locale._format_timeframe('days', 50), '50 يوم') + self.assertEqual(self.locale._format_timeframe('years', 115), '115 سنة') From be5308dd0890642f89e0cb93663b03c29875b518 Mon Sep 17 00:00:00 2001 From: Samer Atiani Date: Mon, 17 Apr 2017 13:28:52 -0400 Subject: [PATCH 093/649] Fix typo in ArabicLocale --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index 4ba0deafc..bf8f98ffd 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1082,7 +1082,7 @@ class ArabicLocale(Locale): 'now': 'الآن', 'seconds': { 'double' : 'ثانيتين', - 'ten' : '{0} ثواني', + 'ten' : '{0} ثوان', 'higher' : '{0} ثانية' }, 'minute': 'دقيقة', From a471d30b32dc7989d8b076f51be822b01249346a Mon Sep 17 00:00:00 2001 From: Samer Atiani Date: Mon, 17 Apr 2017 14:31:29 -0400 Subject: [PATCH 094/649] More accurate month naming per region --- arrow/locales.py | 44 ++++++++++++++++++++++++++++++++++-------- tests/locales_tests.py | 2 +- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index bf8f98ffd..f98877efc 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1071,9 +1071,10 @@ class AzerbaijaniLocale(Locale): day_names = ['', 'Bazar ertəsi', 'Çərşənbə axşamı', 'Çərşənbə', 'Cümə axşamı', 'Cümə', 'Şənbə', 'Bazar'] day_abbreviations = ['', 'Ber', 'Çax', 'Çər', 'Cax', 'Cüm', 'Şnb', 'Bzr'] - -class ArabicLocale(Locale): - names = ['ar', 'ar_eg', 'ar_jo'] +class DefaultArabicLocale(Locale): + names = ['ar', 'ar_ae', 'ar_bh', 'ar_dj', 'ar_eg', 'ar_eh', 'ar_er', + 'ar_km', 'ar_kw', 'ar_ly', 'ar_om', 'ar_qa', 'ar_sa', 'ar_sd', 'ar_so', + 'ar_ss', 'ar_td', 'ar_ye'] past = 'منذ {0}' future = 'خلال {0}' @@ -1117,14 +1118,14 @@ class ArabicLocale(Locale): }, } - month_names = ['', 'كانون الثاني', 'شباط', 'آذار', 'نيسان', 'أيار', 'حزيران', 'تموز', 'آب', - 'أيلول', 'تشرين الأول', 'تشرين الثاني', 'كانون الأول'] - month_abbreviations = ['', 'كانون الثاني', 'شباط', 'آذار', 'نيسان', 'أيار', 'حزيران', 'تموز', 'آب', - 'أيلول', 'تشرين الأول', 'تشرين الثاني', 'كانون الأول'] + month_names = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', + 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] + month_abbreviations = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', + 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] day_names = ['', 'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'] day_abbreviations = ['', 'إثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت', 'أحد'] - + def _format_timeframe(self, timeframe, delta): form = self.timeframes[timeframe] delta = abs(delta) @@ -1138,6 +1139,33 @@ def _format_timeframe(self, timeframe, delta): return form.format(delta) +class LevantArabicLocale(DefaultArabicLocale): + names = ['ar_iq', 'ar_jo', 'ar_lb', 'ar_ps', 'ar_sy'] + month_names = ['', 'كانون الثاني', 'شباط', 'آذار', 'نيسان', 'أيار', 'حزيران', 'تموز', 'آب', + 'أيلول', 'تشرين الأول', 'تشرين الثاني', 'كانون الأول'] + month_abbreviations = ['', 'كانون الثاني', 'شباط', 'آذار', 'نيسان', 'أيار', 'حزيران', 'تموز', 'آب', + 'أيلول', 'تشرين الأول', 'تشرين الثاني', 'كانون الأول'] + +class AlgeriaTunisiaArabicLocale(DefaultArabicLocale): + names = ['ar_tn', 'ar_dz'] + month_names = ['', 'جانفي', 'فيفري', 'مارس', 'أفريل', 'ماي', 'جوان', + 'جويلية', 'أوت', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] + month_abbreviations = ['', 'جانفي', 'فيفري', 'مارس', 'أفريل', 'ماي', 'جوان', + 'جويلية', 'أوت', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] + +class MauritaniaArabicLocale(DefaultArabicLocale): + names = ['ar_mr'] + month_names = ['', 'يناير', 'فبراير', 'مارس', 'إبريل', 'مايو', 'يونيو', + 'يوليو', 'أغشت', 'شتمبر', 'أكتوبر', 'نوفمبر', 'دجمبر'] + month_abbreviations = ['', 'يناير', 'فبراير', 'مارس', 'إبريل', 'مايو', 'يونيو', + 'يوليو', 'أغشت', 'شتمبر', 'أكتوبر', 'نوفمبر', 'دجمبر'] + +class MoroccoArabicLocale(DefaultArabicLocale): + names = ['ar_ma'] + month_names = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'ماي', 'يونيو', + 'يوليوز', 'غشت', 'شتنبر', 'أكتوبر', 'نونبر', 'دجنبر'] + month_abbreviations = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'ماي', 'يونيو', + 'يوليوز', 'غشت', 'شتنبر', 'أكتوبر', 'نونبر', 'دجنبر'] class IcelandicLocale(Locale): diff --git a/tests/locales_tests.py b/tests/locales_tests.py index cca75e4d4..e1e72e2fe 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -558,7 +558,7 @@ class ArabicLocalesTest(Chai): def setUp(self): super(ArabicLocalesTest, self).setUp() - self.locale = locales.ArabicLocale() + self.locale = locales.DefaultArabicLocale() def test_timeframes(self): From 19d214e6273071d87990ce42b6ea6ed524499469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Tue, 25 Apr 2017 10:00:12 +0200 Subject: [PATCH 095/649] Fix a typo in the Basque locales --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index bd1326dca..a368105fb 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1620,7 +1620,7 @@ class BasqueLocale(Locale): month_names = ['', 'urtarrilak', 'otsailak', 'martxoak', 'apirilak', 'maiatzak', 'ekainak', 'uztailak', 'abuztuak', 'irailak', 'urriak', 'azaroak', 'abenduak'] month_abbreviations = ['', 'urt', 'ots', 'mar', 'api', 'mai', 'eka', 'uzt', 'abu', 'ira', 'urr', 'aza', 'abe'] - day_names = ['', 'asteleehna', 'asteartea', 'asteazkena', 'osteguna', 'ostirala', 'larunbata', 'igandea'] + day_names = ['', 'astelehena', 'asteartea', 'asteazkena', 'osteguna', 'ostirala', 'larunbata', 'igandea'] day_abbreviations = ['', 'al', 'ar', 'az', 'og', 'ol', 'lr', 'ig'] From 8314e8ba8afdf76ab2491e1ff4596829a044c5ec Mon Sep 17 00:00:00 2001 From: techgaun Date: Sat, 8 Jul 2017 15:44:58 -0500 Subject: [PATCH 096/649] add nepali locale --- arrow/locales.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index bd1326dca..bbcaab89a 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2002,4 +2002,49 @@ class SlovenianLocale(Locale): ] +class NepaliLocale(Locale): + names = ['ne', 'ne_np'] + + past = '{0} पहिले' + future = '{0} पछी' + + timeframes = { + 'now': 'अहिले', + 'seconds': 'सेकण्ड', + 'minute': 'मिनेट', + 'minutes': '{0} मिनेट', + 'hour': 'एक घण्टा', + 'hours': '{0} घण्टा', + 'day': 'एक दिन', + 'days': '{0} दिन', + 'month': 'एक महिना', + 'months': '{0} महिना', + 'year': 'एक बर्ष', + 'years': 'बर्ष' + } + + meridians = { + 'am': 'पूर्वाह्न', + 'pm': '', + 'AM': 'पूर्वाह्न', + 'PM': '' + } + + month_names = [ + '', 'जनवरी', 'फेब्रुअरी', 'मार्च', 'एप्रील', 'मे', 'जुन', + 'जुलाई', 'अगष्ट', 'सेप्टेम्बर', 'अक्टोबर', 'नोवेम्बर', 'डिसेम्बर' + ] + month_abbreviations = [ + '', 'जन', 'फेब', 'मार्च', 'अप्रिल', 'मे', 'जुन', 'जुलाई', 'अग', + 'सेप', 'अक्ट', 'नोव', 'डिस' + ] + + day_names = [ + 'आइतवार', 'सोमवार', 'मगलवार', 'बुधवार', 'बिहिवार', 'शुक्रवार', 'शनिवार' + ] + + day_abbreviations = [ + 'आइत', 'सोम', 'मगल', 'बुध', 'बिहि', 'शुक्र', 'शनि' + ] + _locales = _map_locales() From 1a84e7ae64ede12db16ee85087eca30f4587947b Mon Sep 17 00:00:00 2001 From: Samar Dhwoj Acharya Date: Sat, 8 Jul 2017 17:54:41 -0500 Subject: [PATCH 097/649] fixes --- arrow/locales.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index bbcaab89a..ea1f9063c 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2031,20 +2031,21 @@ class NepaliLocale(Locale): } month_names = [ - '', 'जनवरी', 'फेब्रुअरी', 'मार्च', 'एप्रील', 'मे', 'जुन', + '', 'जनवरी', 'फेब्रुअरी', 'मार्च', 'एप्रील', 'मे', 'जुन', 'जुलाई', 'अगष्ट', 'सेप्टेम्बर', 'अक्टोबर', 'नोवेम्बर', 'डिसेम्बर' ] month_abbreviations = [ - '', 'जन', 'फेब', 'मार्च', 'अप्रिल', 'मे', 'जुन', 'जुलाई', 'अग', + '', 'जन', 'फेब', 'मार्च', 'एप्रील', 'मे', 'जुन', 'जुलाई', 'अग', 'सेप', 'अक्ट', 'नोव', 'डिस' ] day_names = [ - 'आइतवार', 'सोमवार', 'मगलवार', 'बुधवार', 'बिहिवार', 'शुक्रवार', 'शनिवार' + '', 'सोमवार', 'मंगलवार', 'बुधवार', 'बिहिवार', 'शुक्रवार', 'शनिवार', 'आइतवार' ] day_abbreviations = [ - 'आइत', 'सोम', 'मगल', 'बुध', 'बिहि', 'शुक्र', 'शनि' + '', 'सोम', 'मंगल', 'बुध', 'बिहि', 'शुक्र', 'शनि', 'आइत' ] + _locales = _map_locales() From 601ed1b0893b6a42275139afb69e073f19bd2ea9 Mon Sep 17 00:00:00 2001 From: techgaun Date: Sat, 8 Jul 2017 18:54:30 -0500 Subject: [PATCH 098/649] add tests --- arrow/locales.py | 4 ++-- tests/locales_tests.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index ea1f9063c..6a2b219f0 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2025,9 +2025,9 @@ class NepaliLocale(Locale): meridians = { 'am': 'पूर्वाह्न', - 'pm': '', + 'pm': 'अपरान्ह', 'AM': 'पूर्वाह्न', - 'PM': '' + 'PM': 'अपरान्ह' } month_names = [ diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 1d06fb957..bb00d9193 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -553,3 +553,27 @@ def test_relative_timeframes(self): self.assertEqual(self.locale._format_relative("câteva secunde", "seconds", 1), "peste câteva secunde") self.assertEqual(self.locale._format_relative("o zi", "day", -1), "o zi în urmă") self.assertEqual(self.locale._format_relative("o zi", "day", 1), "peste o zi") + + +class NepaliLocaleTests(Chai): + + def setUp(self): + super(NepaliLocaleTests, self).setUp() + + self.locale = locales.NepaliLocale() + + def test_format_timeframe(self): + assertEqual(self.locale._format_timeframe('hours', 3), '3 घण्टा') + assertEqual(self.locale._format_timeframe('hour', 0), 'एक घण्टा') + + def test_format_relative_now(self): + result = self.locale._format_relative('अहिले', 'now', 0) + assertEqual(result, 'अहिले') + + def test_format_relative_future(self): + result = self.locale._format_relative('एक घण्टा', 'hour', 1) + assertEqual(result, 'एक घण्टा पछी') + + def test_format_relative_past(self): + result = self.locale._format_relative('एक घण्टा', 'hour', -1) + assertEqual(result, 'एक घण्टा पहिले') From 2c2a822b14730115448b6cd4f6f4c16ca44ef911 Mon Sep 17 00:00:00 2001 From: Batista Harahap Date: Wed, 9 Aug 2017 22:27:49 +0700 Subject: [PATCH 099/649] Add Indonesian Locale to arrow --- arrow/locales.py | 39 +++++++++++++++++++++++++++++++++++++++ tests/locales_tests.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index bd1326dca..5da731204 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2001,5 +2001,44 @@ class SlovenianLocale(Locale): '', 'Pon', 'Tor', 'Sre', 'Čet', 'Pet', 'Sob', 'Ned' ] +class IndonesianLocale(Locale): + + names = ['id', 'id_id'] + + past = '{0} yang lalu' + future = 'dalam {0}' + + timeframes = { + 'now': 'baru saja', + 'seconds': 'detik', + 'minute': '1 menit', + 'minutes': '{0} menit', + 'hour': '1 jam', + 'hours': '{0} jam', + 'day': '1 hari', + 'days': '{0} hari', + 'month': '1 bulan', + 'months': '{0} bulan', + 'year': '1 tahun', + 'years': '{0} tahun' + } + + meridians = { + 'am': '', + 'pm': '', + 'AM': '', + 'PM': '' + } + + month_names = ['', 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', + 'Agustus', 'September', 'Oktober', 'November', 'Desember'] + + month_abbreviations = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', + 'Ags', 'Sept', 'Okt', 'Nov', 'Des'] + + day_names = ['', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'] + + day_abbreviations = ['', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'] + _locales = _map_locales() diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 1d06fb957..4f3941994 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -553,3 +553,32 @@ def test_relative_timeframes(self): self.assertEqual(self.locale._format_relative("câteva secunde", "seconds", 1), "peste câteva secunde") self.assertEqual(self.locale._format_relative("o zi", "day", -1), "o zi în urmă") self.assertEqual(self.locale._format_relative("o zi", "day", 1), "peste o zi") + +class IndonesianLocaleTests(Chai): + + def setUp(self): + super(IndonesianLocaleTests, self).setUp() + + self.locale = locales.IndonesianLocale() + + def test_timeframes(self): + self.assertEqual(self.locale._format_timeframe('hours', 2), '2 jam') + self.assertEqual(self.locale._format_timeframe('months', 2), '2 bulan') + + self.assertEqual(self.locale._format_timeframe('days', 2), '2 hari') + self.assertEqual(self.locale._format_timeframe('years', 2), '2 tahun') + + self.assertEqual(self.locale._format_timeframe('hours', 3), '3 jam') + self.assertEqual(self.locale._format_timeframe('months', 4), '4 bulan') + self.assertEqual(self.locale._format_timeframe('days', 3), '3 hari') + self.assertEqual(self.locale._format_timeframe('years', 5), '5 tahun') + + def test_format_relative_now(self): + self.assertEqual(self.locale._format_relative('baru saja', 'now', 0), 'baru saja') + + def test_format_relative_past(self): + self.assertEqual(self.locale._format_relative('1 jam', 'hour', 1), 'dalam 1 jam') + self.assertEqual(self.locale._format_relative('1 detik', 'seconds', 1), 'dalam 1 detik') + + def test_format_relative_future(self): + self.assertEqual(self.locale._format_relative('1 jam', 'hour', -1), '1 jam yang lalu') From a0fdca41ca4d0fd15437daab2eb36788038c9161 Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Thu, 14 Sep 2017 20:49:07 -0400 Subject: [PATCH 100/649] pip's --use-mirrors flag has been removed since 7.0.0 (2015-05-21) --- Makefile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index bd5622a40..e1a3b2cf5 100644 --- a/Makefile +++ b/Makefile @@ -4,25 +4,25 @@ auto: build27 build27: virtualenv local --python=python2.7 - local/bin/pip install --use-mirrors -r requirements.txt + local/bin/pip install -r requirements.txt build26: virtualenv local --python=python2.6 - local/bin/pip install --use-mirrors -r requirements.txt - local/bin/pip install --use-mirrors -r requirements26.txt + local/bin/pip install -r requirements.txt + local/bin/pip install -r requirements26.txt build33: virtualenv local --python=python3.3 - local/bin/pip install --use-mirrors -r requirements.txt + local/bin/pip install -r requirements.txt build34: virtualenv local --python=python3.4 - local/bin/pip install --use-mirrors -r requirements.txt + local/bin/pip install -r requirements.txt build35: virtualenv local --python=python3.5 - local/bin/pip install --use-mirrors -r requirements.txt + local/bin/pip install -r requirements.txt test: rm -f .coverage From e2dac958b5e2b7de0ddd217aad9325170274c237 Mon Sep 17 00:00:00 2001 From: Ramon Saraiva Date: Fri, 15 Sep 2017 11:47:10 -0300 Subject: [PATCH 101/649] Fixed pt_br past --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index 5da731204..95b4def70 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -945,7 +945,7 @@ class PortugueseLocale(Locale): class BrazilianPortugueseLocale(PortugueseLocale): names = ['pt_br'] - past = 'fazem {0}' + past = 'faz {0}' class TagalogLocale(Locale): From c5c89b5dadf5335dd6ed3aab66b564729592b1c5 Mon Sep 17 00:00:00 2001 From: Roman Inflianskas Date: Sat, 17 Jun 2017 20:09:52 +0300 Subject: [PATCH 102/649] Return generator instead of list in Arrow.range(), Arrow.span_range(), and Arrow.interval(). Fix #251. --- arrow/arrow.py | 17 +++++------ tests/arrow_tests.py | 72 ++++++++++++++++++++++---------------------- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 7cd5dcb1f..7585b53b1 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -192,7 +192,7 @@ def strptime(cls, date_str, fmt, tzinfo=None): @classmethod def range(cls, frame, start, end=None, tz=None, limit=None): - ''' Returns a list of :class:`Arrow ` objects, representing + ''' Returns a generator of :class:`Arrow ` objects, representing an iteration of time between two inputs. :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). @@ -251,16 +251,15 @@ def range(cls, frame, start, end=None, tz=None, limit=None): end = cls._get_datetime(end).replace(tzinfo=tzinfo) current = cls.fromdatetime(start) - results = [] + i = 0 - while current <= end and len(results) < limit: - results.append(current) + while current <= end and i < limit: + i += 1 + yield current values = [getattr(current, f) for f in cls._ATTRS] current = cls(*values, tzinfo=tzinfo) + relativedelta(**{frame_relative: relative_steps}) - return results - @classmethod def span_range(cls, frame, start, end, tz=None, limit=None): @@ -311,7 +310,7 @@ def span_range(cls, frame, start, end, tz=None, limit=None): tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) start = cls.fromdatetime(start, tzinfo).span(frame)[0] _range = cls.range(frame, start, end, tz, limit) - return [r.span(frame) for r in _range] + return (r.span(frame) for r in _range) @classmethod def interval(cls, frame, start, end, interval=1, tz=None): @@ -352,10 +351,10 @@ def interval(cls, frame, start, end, interval=1, tz=None): if interval < 1: raise ValueError("interval has to be a positive integer") - spanRange = cls.span_range(frame,start,end,tz) + spanRange = list(cls.span_range(frame, start, end, tz)) bound = (len(spanRange) // interval) * interval - return [ (spanRange[i][0],spanRange[i+ interval - 1][1]) for i in range(0,bound, interval) ] + return ((spanRange[i][0], spanRange[i + interval - 1][1]) for i in range(0, bound, interval)) # representations diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index e5a8b5223..7f3e041ef 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -691,8 +691,8 @@ class ArrowRangeTests(Chai): def test_year(self): - result = arrow.Arrow.range('year', datetime(2013, 1, 2, 3, 4, 5), - datetime(2016, 4, 5, 6, 7, 8)) + result = list(arrow.Arrow.range('year', datetime(2013, 1, 2, 3, 4, 5), + datetime(2016, 4, 5, 6, 7, 8))) assertEqual(result, [ arrow.Arrow(2013, 1, 2, 3, 4, 5), @@ -703,8 +703,8 @@ def test_year(self): def test_quarter(self): - result = arrow.Arrow.range('quarter', datetime(2013, 2, 3, 4, 5, 6), - datetime(2013, 5, 6, 7, 8, 9)) + result = list(arrow.Arrow.range('quarter', datetime(2013, 2, 3, 4, 5, 6), + datetime(2013, 5, 6, 7, 8, 9))) assertEqual(result, [ arrow.Arrow(2013, 2, 3, 4, 5, 6), @@ -713,8 +713,8 @@ def test_quarter(self): def test_month(self): - result = arrow.Arrow.range('month', datetime(2013, 2, 3, 4, 5, 6), - datetime(2013, 5, 6, 7, 8, 9)) + result = list(arrow.Arrow.range('month', datetime(2013, 2, 3, 4, 5, 6), + datetime(2013, 5, 6, 7, 8, 9))) assertEqual(result, [ arrow.Arrow(2013, 2, 3, 4, 5, 6), @@ -725,8 +725,8 @@ def test_month(self): def test_week(self): - result = arrow.Arrow.range('week', datetime(2013, 9, 1, 2, 3, 4), - datetime(2013, 10, 1, 2, 3, 4)) + result = list(arrow.Arrow.range('week', datetime(2013, 9, 1, 2, 3, 4), + datetime(2013, 10, 1, 2, 3, 4))) assertEqual(result, [ arrow.Arrow(2013, 9, 1, 2, 3, 4), @@ -738,8 +738,8 @@ def test_week(self): def test_day(self): - result = arrow.Arrow.range('day', datetime(2013, 1, 2, 3, 4, 5), - datetime(2013, 1, 5, 6, 7, 8)) + result = list(arrow.Arrow.range('day', datetime(2013, 1, 2, 3, 4, 5), + datetime(2013, 1, 5, 6, 7, 8))) assertEqual(result, [ arrow.Arrow(2013, 1, 2, 3, 4, 5), @@ -750,8 +750,8 @@ def test_day(self): def test_hour(self): - result = arrow.Arrow.range('hour', datetime(2013, 1, 2, 3, 4, 5), - datetime(2013, 1, 2, 6, 7, 8)) + result = list(arrow.Arrow.range('hour', datetime(2013, 1, 2, 3, 4, 5), + datetime(2013, 1, 2, 6, 7, 8))) assertEqual(result, [ arrow.Arrow(2013, 1, 2, 3, 4, 5), @@ -760,8 +760,8 @@ def test_hour(self): arrow.Arrow(2013, 1, 2, 6, 4, 5), ]) - result = arrow.Arrow.range('hour', datetime(2013, 1, 2, 3, 4, 5), - datetime(2013, 1, 2, 3, 4, 5)) + result = list(arrow.Arrow.range('hour', datetime(2013, 1, 2, 3, 4, 5), + datetime(2013, 1, 2, 3, 4, 5))) assertEqual(result, [ arrow.Arrow(2013, 1, 2, 3, 4, 5), @@ -769,8 +769,8 @@ def test_hour(self): def test_minute(self): - result = arrow.Arrow.range('minute', datetime(2013, 1, 2, 3, 4, 5), - datetime(2013, 1, 2, 3, 7, 8)) + result = list(arrow.Arrow.range('minute', datetime(2013, 1, 2, 3, 4, 5), + datetime(2013, 1, 2, 3, 7, 8))) assertEqual(result, [ arrow.Arrow(2013, 1, 2, 3, 4, 5), @@ -781,8 +781,8 @@ def test_minute(self): def test_second(self): - result = arrow.Arrow.range('second', datetime(2013, 1, 2, 3, 4, 5), - datetime(2013, 1, 2, 3, 4, 8)) + result = list(arrow.Arrow.range('second', datetime(2013, 1, 2, 3, 4, 5), + datetime(2013, 1, 2, 3, 4, 8))) assertEqual(result, [ arrow.Arrow(2013, 1, 2, 3, 4, 5), @@ -793,8 +793,8 @@ def test_second(self): def test_arrow(self): - result = arrow.Arrow.range('day', arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 5, 6, 7, 8)) + result = list(arrow.Arrow.range('day', arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 5, 6, 7, 8))) assertEqual(result, [ arrow.Arrow(2013, 1, 2, 3, 4, 5), @@ -838,14 +838,14 @@ def test_aware_tz(self): def test_unsupported(self): with assertRaises(AttributeError): - arrow.Arrow.range('abc', datetime.utcnow(), datetime.utcnow()) + next(arrow.Arrow.range('abc', datetime.utcnow(), datetime.utcnow())) class ArrowSpanRangeTests(Chai): def test_year(self): - result = arrow.Arrow.span_range('year', datetime(2013, 2, 1), datetime(2016, 3, 31)) + result = list(arrow.Arrow.span_range('year', datetime(2013, 2, 1), datetime(2016, 3, 31))) assertEqual(result, [ (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 12, 31, 23, 59, 59, 999999)), @@ -856,7 +856,7 @@ def test_year(self): def test_quarter(self): - result = arrow.Arrow.span_range('quarter', datetime(2013, 2, 2), datetime(2013, 5, 15)) + result = list(arrow.Arrow.span_range('quarter', datetime(2013, 2, 2), datetime(2013, 5, 15))) assertEqual(result, [ (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)), @@ -865,7 +865,7 @@ def test_quarter(self): def test_month(self): - result = arrow.Arrow.span_range('month', datetime(2013, 1, 2), datetime(2013, 4, 15)) + result = list(arrow.Arrow.span_range('month', datetime(2013, 1, 2), datetime(2013, 4, 15))) assertEqual(result, [ (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 1, 31, 23, 59, 59, 999999)), @@ -876,7 +876,7 @@ def test_month(self): def test_week(self): - result = arrow.Arrow.span_range('week', datetime(2013, 2, 2), datetime(2013, 2, 28)) + result = list(arrow.Arrow.span_range('week', datetime(2013, 2, 2), datetime(2013, 2, 28))) assertEqual(result, [ (arrow.Arrow(2013, 1, 28), arrow.Arrow(2013, 2, 3, 23, 59, 59, 999999)), @@ -889,8 +889,8 @@ def test_week(self): def test_day(self): - result = arrow.Arrow.span_range('day', datetime(2013, 1, 1, 12), - datetime(2013, 1, 4, 12)) + result = list(arrow.Arrow.span_range('day', datetime(2013, 1, 1, 12), + datetime(2013, 1, 4, 12))) assertEqual(result, [ (arrow.Arrow(2013, 1, 1, 0), arrow.Arrow(2013, 1, 1, 23, 59, 59, 999999)), @@ -901,8 +901,8 @@ def test_day(self): def test_hour(self): - result = arrow.Arrow.span_range('hour', datetime(2013, 1, 1, 0, 30), - datetime(2013, 1, 1, 3, 30)) + result = list(arrow.Arrow.span_range('hour', datetime(2013, 1, 1, 0, 30), + datetime(2013, 1, 1, 3, 30))) assertEqual(result, [ (arrow.Arrow(2013, 1, 1, 0), arrow.Arrow(2013, 1, 1, 0, 59, 59, 999999)), @@ -911,8 +911,8 @@ def test_hour(self): (arrow.Arrow(2013, 1, 1, 3), arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999)), ]) - result = arrow.Arrow.span_range('hour', datetime(2013, 1, 1, 3, 30), - datetime(2013, 1, 1, 3, 30)) + result = list(arrow.Arrow.span_range('hour', datetime(2013, 1, 1, 3, 30), + datetime(2013, 1, 1, 3, 30))) assertEqual(result, [ (arrow.Arrow(2013, 1, 1, 3), arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999)), @@ -920,8 +920,8 @@ def test_hour(self): def test_minute(self): - result = arrow.Arrow.span_range('minute', datetime(2013, 1, 1, 0, 0, 30), - datetime(2013, 1, 1, 0, 3, 30)) + result = list(arrow.Arrow.span_range('minute', datetime(2013, 1, 1, 0, 0, 30), + datetime(2013, 1, 1, 0, 3, 30))) assertEqual(result, [ (arrow.Arrow(2013, 1, 1, 0, 0), arrow.Arrow(2013, 1, 1, 0, 0, 59, 999999)), @@ -932,8 +932,8 @@ def test_minute(self): def test_second(self): - result = arrow.Arrow.span_range('second', datetime(2013, 1, 1), - datetime(2013, 1, 1, 0, 0, 3)) + result = list(arrow.Arrow.span_range('second', datetime(2013, 1, 1), + datetime(2013, 1, 1, 0, 0, 3))) assertEqual(result, [ (arrow.Arrow(2013, 1, 1, 0, 0, 0), arrow.Arrow(2013, 1, 1, 0, 0, 0, 999999)), @@ -999,7 +999,7 @@ def test_incorrect_input(self): assertEqual(correct,False) def test_correct(self): - result = arrow.Arrow.interval('hour', datetime(2013, 5, 5, 12, 30), datetime(2013, 5, 5, 17, 15),2) + result = list(arrow.Arrow.interval('hour', datetime(2013, 5, 5, 12, 30), datetime(2013, 5, 5, 17, 15), 2)) assertEqual(result,[(arrow.Arrow(2013, 5, 5, 12), arrow.Arrow(2013, 5, 5, 13, 59, 59, 999999)), (arrow.Arrow(2013, 5, 5, 14), arrow.Arrow(2013, 5, 5, 15, 59, 59, 999999)), From 8b7d51fb0531671f16275064e866304cc96accf4 Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Sun, 17 Sep 2017 16:07:48 -0400 Subject: [PATCH 103/649] Update .gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 56d9632ab..068d72c92 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,9 @@ .coverage nosetests.xml +tests/.noseids +.venv/ local/ dist/ docs/_build/ @@ -19,3 +21,6 @@ docs/_build/ # tox .tox + +# VS Code +.vscode/ From b2e568e2f3c7b4d58cad14fd7af1a875c54996ce Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Sun, 17 Sep 2017 17:51:16 -0400 Subject: [PATCH 104/649] Make Arrow.interval() return an iterator --- arrow/arrow.py | 38 ++++++++++++++++++++------------------ tests/arrow_tests.py | 20 ++++++++++---------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 7585b53b1..95c444543 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -314,7 +314,7 @@ def span_range(cls, frame, start, end, tz=None, limit=None): @classmethod def interval(cls, frame, start, end, interval=1, tz=None): - ''' Returns an array of tuples, each :class:`Arrow ` objects, + ''' Returns an iterator of tuples, each :class:`Arrow ` objects, representing a series of intervals between two inputs. :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). @@ -322,7 +322,7 @@ def interval(cls, frame, start, end, interval=1, tz=None): :param end: (optional) A datetime expression, the end of the range. :param interval: (optional) Time interval for the given time frame. :param tz: (optional) A timezone expression. Defaults to UTC. - + Supported frame values: year, quarter, month, week, day, hour, minute, second Recognized datetime expressions: @@ -351,10 +351,12 @@ def interval(cls, frame, start, end, interval=1, tz=None): if interval < 1: raise ValueError("interval has to be a positive integer") - spanRange = list(cls.span_range(frame, start, end, tz)) - - bound = (len(spanRange) // interval) * interval - return ((spanRange[i][0], spanRange[i + interval - 1][1]) for i in range(0, bound, interval)) + spanRange = cls.span_range(frame, start, end, tz) + while True: + intvlStart, intvlEnd = next(spanRange) # StopIteration when exhausted + for _ in range(interval-1): + _, intvlEnd = next(spanRange) # StopIteration when exhausted + yield intvlStart, intvlEnd # representations @@ -741,43 +743,43 @@ def humanize(self, other=None, locale='en_us', only_distance=False, granularity= sign = -1 if delta < 0 else 1 diff = abs(delta) delta = diff - + if granularity=='auto': if diff < 10: return locale.describe('now', only_distance=only_distance) - + if diff < 45: seconds = sign * delta return locale.describe('seconds', seconds, only_distance=only_distance) - + elif diff < 90: return locale.describe('minute', sign, only_distance=only_distance) elif diff < 2700: minutes = sign * int(max(delta / 60, 2)) return locale.describe('minutes', minutes, only_distance=only_distance) - + elif diff < 5400: return locale.describe('hour', sign, only_distance=only_distance) elif diff < 79200: hours = sign * int(max(delta / 3600, 2)) return locale.describe('hours', hours, only_distance=only_distance) - + elif diff < 129600: return locale.describe('day', sign, only_distance=only_distance) elif diff < 2160000: days = sign * int(max(delta / 86400, 2)) return locale.describe('days', days, only_distance=only_distance) - + elif diff < 3888000: return locale.describe('month', sign, only_distance=only_distance) elif diff < 29808000: self_months = self._datetime.year * 12 + self._datetime.month other_months = dt.year * 12 + dt.month - + months = sign * int(max(abs(other_months - self_months), 2)) - + return locale.describe('months', months, only_distance=only_distance) - + elif diff < 47260800: return locale.describe('year', sign, only_distance=only_distance) else: @@ -801,8 +803,8 @@ def humanize(self, other=None, locale='en_us', only_distance=False, granularity= delta = sign * delta / float(60*60*24*365.25) else: raise AttributeError('Error. Could not understand your level of granularity. Please select between \ - "second", "minute", "hour", "day", "week", "month" or "year"') - + "second", "minute", "hour", "day", "week", "month" or "year"') + if(trunc(abs(delta)) != 1): granularity += 's' return locale.describe(granularity, delta, only_distance=False) @@ -828,7 +830,7 @@ def __sub__(self, other): elif isinstance(other, Arrow): return self._datetime - other._datetime - + raise TypeError() def __rsub__(self, other): diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 7f3e041ef..eb20a6aec 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -992,7 +992,7 @@ class ArrowIntervalTests(Chai): def test_incorrect_input(self): correct = True try: - result = arrow.Arrow.interval('month', datetime(2013, 1, 2), datetime(2013, 4, 15),0) + result = list(arrow.Arrow.interval('month', datetime(2013, 1, 2), datetime(2013, 4, 15), 0)) except: correct = False @@ -1116,15 +1116,15 @@ def setUp(self): self.now = arrow.Arrow.utcnow() def test_granularity(self): - + assertEqual(self.now.humanize(granularity = 'second'), 'just now') - + later1 = self.now.shift(seconds=1) assertEqual(self.now.humanize(later1, granularity = 'second'), 'just now') assertEqual(later1.humanize(self.now, granularity = 'second'), 'just now') assertEqual(self.now.humanize(later1, granularity = 'minute'), '0 minutes ago') assertEqual(later1.humanize(self.now, granularity = 'minute'), 'in 0 minutes') - + later100 = self.now.shift(seconds=100) assertEqual(self.now.humanize(later100, granularity = 'second'), 'seconds ago') assertEqual(later100.humanize(self.now, granularity = 'second'), 'in seconds') @@ -1132,7 +1132,7 @@ def test_granularity(self): assertEqual(later100.humanize(self.now, granularity = 'minute'), 'in a minute') assertEqual(self.now.humanize(later100, granularity = 'hour'), '0 hours ago') assertEqual(later100.humanize(self.now, granularity = 'hour'), 'in 0 hours') - + later4000 = self.now.shift(seconds=4000) assertEqual(self.now.humanize(later4000, granularity = 'minute'), '66 minutes ago') assertEqual(later4000.humanize(self.now, granularity = 'minute'), 'in 66 minutes') @@ -1140,7 +1140,7 @@ def test_granularity(self): assertEqual(later4000.humanize(self.now, granularity = 'hour'), 'in an hour') assertEqual(self.now.humanize(later4000, granularity = 'day'), '0 days ago') assertEqual(later4000.humanize(self.now, granularity = 'day'), 'in 0 days') - + later105 = self.now.shift(seconds=10 ** 5) assertEqual(self.now.humanize(later105, granularity = 'hour'), '27 hours ago') assertEqual(later105.humanize(self.now, granularity = 'hour'), 'in 27 hours') @@ -1148,7 +1148,7 @@ def test_granularity(self): assertEqual(later105.humanize(self.now, granularity = 'day'), 'in a day') assertEqual(self.now.humanize(later105, granularity = 'month'), '0 months ago') assertEqual(later105.humanize(self.now, granularity = 'month'), 'in 0 months') - + later106 = self.now.shift(seconds=3 * 10 ** 6) assertEqual(self.now.humanize(later106, granularity = 'day'), '34 days ago') assertEqual(later106.humanize(self.now, granularity = 'day'), 'in 34 days') @@ -1156,19 +1156,19 @@ def test_granularity(self): assertEqual(later106.humanize(self.now, granularity = 'month'), 'in a month') assertEqual(self.now.humanize(later106, granularity = 'year'), '0 years ago') assertEqual(later106.humanize(self.now, granularity = 'year'), 'in 0 years') - + later506 = self.now.shift(seconds=50 * 10 ** 6) assertEqual(self.now.humanize(later506, granularity = 'month'), '18 months ago') assertEqual(later506.humanize(self.now, granularity = 'month'), 'in 18 months') assertEqual(self.now.humanize(later506, granularity = 'year'), 'a year ago') assertEqual(later506.humanize(self.now, granularity = 'year'), 'in a year') - + later108 = self.now.shift(seconds=10 ** 8) assertEqual(self.now.humanize(later108, granularity = 'year'), '3 years ago') assertEqual(later108.humanize(self.now, granularity = 'year'), 'in 3 years') with assertRaises(AttributeError): self.now.humanize(later108, granularity = 'years') - + def test_seconds(self): later = self.now.shift(seconds=10) From 6a3ca5f12b32181bd74e509b330ced75beaca05c Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Sat, 23 Sep 2017 10:17:15 -0400 Subject: [PATCH 105/649] Add shims around range() et al to warn of switch to iterables Based off an idea from @pganssle, this new shim object will raise a DeprecationWarning on any operation not valid on iterables...which is basically everything except calling iter(shim) **once**. --- arrow/arrow.py | 11 +++++--- arrow/util.py | 66 +++++++++++++++++++++++++++++++++++++++++++- tests/arrow_tests.py | 57 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 5 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 95c444543..98841984b 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -191,9 +191,10 @@ def strptime(cls, date_str, fmt, tzinfo=None): # factories: ranges and spans @classmethod + @util.list_to_iter_deprecation def range(cls, frame, start, end=None, tz=None, limit=None): - ''' Returns a generator of :class:`Arrow ` objects, representing - an iteration of time between two inputs. + ''' Returns an iterator of :class:`Arrow ` objects, representing + points in time between two inputs. :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). :param start: A datetime expression, the start of the range. @@ -262,8 +263,9 @@ def range(cls, frame, start, end=None, tz=None, limit=None): @classmethod + @util.list_to_iter_deprecation def span_range(cls, frame, start, end, tz=None, limit=None): - ''' Returns a list of tuples, each :class:`Arrow ` objects, + ''' Returns an iterator of tuples, each :class:`Arrow ` objects, representing a series of timespans between two inputs. :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). @@ -313,6 +315,7 @@ def span_range(cls, frame, start, end, tz=None, limit=None): return (r.span(frame) for r in _range) @classmethod + @util.list_to_iter_deprecation def interval(cls, frame, start, end, interval=1, tz=None): ''' Returns an iterator of tuples, each :class:`Arrow ` objects, representing a series of intervals between two inputs. @@ -351,7 +354,7 @@ def interval(cls, frame, start, end, interval=1, tz=None): if interval < 1: raise ValueError("interval has to be a positive integer") - spanRange = cls.span_range(frame, start, end, tz) + spanRange = iter(cls.span_range(frame, start, end, tz)) while True: intvlStart, intvlEnd = next(spanRange) # StopIteration when exhausted for _ in range(interval-1): diff --git a/arrow/util.py b/arrow/util.py index 3eed4faa8..9b75ef334 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import +import functools import sys +import warnings # python 2.6 / 2.7 definitions for total_seconds function. @@ -44,4 +46,66 @@ def isstr(s): return isinstance(s, str) -__all__ = ['total_seconds', 'is_timestamp', 'isstr'] +class list_to_iter_shim(list): + ''' A temporary shim for functions that currently return a list but that will, after a + deprecation period, return an iteratator. + ''' + + def __init__(self, iterable=(), *, warn_text=None): + ''' Equivalent to list(iterable). warn_text will be emitted on all non-iterator operations. + ''' + self._warn_text = warn_text or 'this object will be converted to an iterator in a future release' + self._iter_count = 0 + list.__init__(self, iterable) + + def _warn(self): + warnings.warn(self._warn_text, DeprecationWarning) + + @functools.wraps(list.__iter__) + def __iter__(self): + self._iter_count += 1 + if self._iter_count > 1: + self._warn() + return list.__iter__(self) + + def _wrap_method(name): + list_func = getattr(list, name) + @functools.wraps(list_func) + def wrapper(self, *args, **kwargs): + self._warn() + return list_func(self, *args, **kwargs) + return wrapper + + __contains__ = _wrap_method('__contains__') + __add__ = _wrap_method('__add__') + __mul__ = _wrap_method('__mul__') + __getitem__ = _wrap_method('__getitem__') + # Ideally, we would throw warnings from __len__, but list(x) calls len(x) + index = _wrap_method('index') + count = _wrap_method('count') + __setitem__ = _wrap_method('__setitem__') + __delitem__ = _wrap_method('__delitem__') + append = _wrap_method('append') + clear = _wrap_method('clear') + copy = _wrap_method('copy') + extend = _wrap_method('extend') + __iadd__ = _wrap_method('__iadd__') + __imul__ = _wrap_method('__imul__') + insert = _wrap_method('insert') + pop = _wrap_method('pop') + remove = _wrap_method('remove') + reverse = _wrap_method('reverse') + sort = _wrap_method('sort') + + del _wrap_method + + +def list_to_iter_deprecation(f): + warn_text = '{0}() will return an iterator in a future release, convert to list({0}())'.format(f.__name__) + @functools.wraps(f) + def wrapper(*args, **kwargs): + return list_to_iter_shim(f(*args, **kwargs), warn_text=warn_text) + return wrapper + + +__all__ = ['total_seconds', 'is_timestamp', 'isstr', 'list_to_iter_shim', 'list_to_iter_deprecation'] diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index eb20a6aec..369554da5 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1401,3 +1401,60 @@ def test_get_iteration_params(self): with assertRaises(Exception): arrow.Arrow._get_iteration_params(None, None) + + def test_list_to_iter_shim(self): + def newshim(): + return util.list_to_iter_shim(range(5), warn_text='testing') + + # Iterating over a shim once should not throw a warning + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + iter(newshim()) + list(newshim()) + for _ in newshim(): pass + + assertEqual([], w) + + # Iterating over a shim twice (or more) should throw a warning + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + shim = newshim() + + for _ in shim: pass + for _ in shim: pass + + assertEqual(1, len(w)) + assertEqual(w[0].category, DeprecationWarning) + assertEqual("testing", w[0].message.args[0]) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + shim = newshim() + + 0 in shim + shim + [] + shim * 1 + shim[0] + len(shim) + shim.index(0) + shim.count(0) + + shim[0:0] = [] + del shim[0:0] + newshim().append(6) + newshim().clear() + shim.copy() + shim.extend([]) + shim += [] + shim *= 1 + newshim().insert(0, 6) + shim.pop(-1) + newshim().remove(0) + newshim().reverse() + newshim().sort() + + assertEqual(19, len(w)) + for warn in w: + assertEqual(warn.category, DeprecationWarning) + assertEqual("testing", warn.message.args[0]) From 60461849b5909f90f542c83c2024733ecc2b7002 Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Sat, 23 Sep 2017 10:27:07 -0400 Subject: [PATCH 106/649] Tiny documentation tweaks --- arrow/arrow.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 98841984b..34747924a 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -196,7 +196,7 @@ def range(cls, frame, start, end=None, tz=None, limit=None): ''' Returns an iterator of :class:`Arrow ` objects, representing points in time between two inputs. - :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). :param start: A datetime expression, the start of the range. :param end: (optional) A datetime expression, the end of the range. :param tz: (optional) A :ref:`timezone expression `. Defaults to @@ -231,7 +231,7 @@ def range(cls, frame, start, end=None, tz=None, limit=None): - **NOTE**: Unlike Python's ``range``, ``end`` *may* be included in the returned list:: + **NOTE**: Unlike Python's ``range``, ``end`` *may* be included in the returned iterator:: >>> start = datetime(2013, 5, 5, 12, 30) >>> end = datetime(2013, 5, 5, 13, 30) @@ -268,7 +268,7 @@ def span_range(cls, frame, start, end, tz=None, limit=None): ''' Returns an iterator of tuples, each :class:`Arrow ` objects, representing a series of timespans between two inputs. - :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). :param start: A datetime expression, the start of the range. :param end: (optional) A datetime expression, the end of the range. :param tz: (optional) A :ref:`timezone expression `. Defaults to @@ -290,8 +290,8 @@ def span_range(cls, frame, start, end, tz=None, limit=None): - An :class:`Arrow ` object. - A ``datetime`` object. - **NOTE**: Unlike Python's ``range``, ``end`` will *always* be included in the returned list - of timespans. + **NOTE**: Unlike Python's ``range``, ``end`` will *always* be included in the returned + iterator of timespans. Usage: @@ -320,7 +320,7 @@ def interval(cls, frame, start, end, interval=1, tz=None): ''' Returns an iterator of tuples, each :class:`Arrow ` objects, representing a series of intervals between two inputs. - :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). + :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). :param start: A datetime expression, the start of the range. :param end: (optional) A datetime expression, the end of the range. :param interval: (optional) Time interval for the given time frame. From c8af8f8f1c4205c31e7785ce80cc44cc9d42eb9d Mon Sep 17 00:00:00 2001 From: Sye van der Veen Date: Mon, 25 Sep 2017 16:36:00 -0400 Subject: [PATCH 107/649] Fix broken Python 2 support --- arrow/util.py | 13 ++++++------- tests/arrow_tests.py | 16 ++++++++++------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/arrow/util.py b/arrow/util.py index 9b75ef334..088e5dfd6 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -51,17 +51,16 @@ class list_to_iter_shim(list): deprecation period, return an iteratator. ''' - def __init__(self, iterable=(), *, warn_text=None): + def __init__(self, iterable=(), **kwargs): ''' Equivalent to list(iterable). warn_text will be emitted on all non-iterator operations. ''' - self._warn_text = warn_text or 'this object will be converted to an iterator in a future release' + self._warn_text = kwargs.pop('warn_text', None) or 'this object will be converted to an iterator in a future release' self._iter_count = 0 - list.__init__(self, iterable) + list.__init__(self, iterable, **kwargs) def _warn(self): warnings.warn(self._warn_text, DeprecationWarning) - @functools.wraps(list.__iter__) def __iter__(self): self._iter_count += 1 if self._iter_count > 1: @@ -70,7 +69,6 @@ def __iter__(self): def _wrap_method(name): list_func = getattr(list, name) - @functools.wraps(list_func) def wrapper(self, *args, **kwargs): self._warn() return list_func(self, *args, **kwargs) @@ -86,8 +84,9 @@ def wrapper(self, *args, **kwargs): __setitem__ = _wrap_method('__setitem__') __delitem__ = _wrap_method('__delitem__') append = _wrap_method('append') - clear = _wrap_method('clear') - copy = _wrap_method('copy') + if version >= '3.0': # pragma: no cover + clear = _wrap_method('clear') + copy = _wrap_method('copy') extend = _wrap_method('extend') __iadd__ = _wrap_method('__iadd__') __imul__ = _wrap_method('__imul__') diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 369554da5..673456179 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1413,6 +1413,7 @@ def newshim(): iter(newshim()) list(newshim()) for _ in newshim(): pass + len(newshim()) # ...because it's called by `list(x)` assertEqual([], w) @@ -1436,15 +1437,15 @@ def newshim(): shim + [] shim * 1 shim[0] - len(shim) shim.index(0) shim.count(0) - shim[0:0] = [] - del shim[0:0] + shim[0:0] = [] # doesn't warn on py2 + del shim[0:0] # doesn't warn on py2 newshim().append(6) - newshim().clear() - shim.copy() + if util.version >= '3.0': # pragma: no cover + newshim().clear() + shim.copy() shim.extend([]) shim += [] shim *= 1 @@ -1454,7 +1455,10 @@ def newshim(): newshim().reverse() newshim().sort() - assertEqual(19, len(w)) + if util.version >= '3.0': # pragma: no cover + assertEqual(19, len(w)) + else: # pragma: no cover + assertEqual(15, len(w)) for warn in w: assertEqual(warn.category, DeprecationWarning) assertEqual("testing", warn.message.args[0]) From 5cdcd61bf6957f4850fe8379c67f5e05b53b0079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20Fuxj=C3=A4ger?= Date: Thu, 28 Sep 2017 16:55:14 +0200 Subject: [PATCH 108/649] Changed AustriaLocale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * renamed AustriaLocale to AustrianLocale * changed name of first month to "Jänner" --- arrow/locales.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index 0c4cecc74..8a00c27a4 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -840,10 +840,12 @@ class GermanLocale(_DeutschLocaleCommonMixin, Locale): names = ['de', 'de_de'] -class AustriaLocale(_DeutschLocaleCommonMixin, Locale): +class AustrianLocale(_DeutschLocaleCommonMixin, Locale): names = ['de_at'] + month_names[1] = 'Jänner' + class NorwegianLocale(Locale): From 9c2ca434a2dd58f234cac06cf40baf7bb59138e6 Mon Sep 17 00:00:00 2001 From: Max F Date: Thu, 28 Sep 2017 17:17:42 +0200 Subject: [PATCH 109/649] Fixed a build issue Explicitely replace `month_names` of the _DeutschLocaleCommonMixin --- arrow/locales.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index 8a00c27a4..b5c8d5bda 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -844,7 +844,10 @@ class AustrianLocale(_DeutschLocaleCommonMixin, Locale): names = ['de_at'] - month_names[1] = 'Jänner' + month_names = [ + '', 'Jänner', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', + 'August', 'September', 'Oktober', 'November', 'Dezember' + ] class NorwegianLocale(Locale): From 937a9556ad2a3f2cfb129bdd05e8e1fecb66016b Mon Sep 17 00:00:00 2001 From: Samer Atiani Date: Fri, 29 Sep 2017 11:47:34 -0400 Subject: [PATCH 110/649] Rename DefaultArabicLocale to ArabicLocale --- arrow/locales.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 548524039..724a3d197 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1076,7 +1076,7 @@ class AzerbaijaniLocale(Locale): day_names = ['', 'Bazar ertəsi', 'Çərşənbə axşamı', 'Çərşənbə', 'Cümə axşamı', 'Cümə', 'Şənbə', 'Bazar'] day_abbreviations = ['', 'Ber', 'Çax', 'Çər', 'Cax', 'Cüm', 'Şnb', 'Bzr'] -class DefaultArabicLocale(Locale): +class ArabicLocale(Locale): names = ['ar', 'ar_ae', 'ar_bh', 'ar_dj', 'ar_eg', 'ar_eh', 'ar_er', 'ar_km', 'ar_kw', 'ar_ly', 'ar_om', 'ar_qa', 'ar_sa', 'ar_sd', 'ar_so', 'ar_ss', 'ar_td', 'ar_ye'] @@ -1144,28 +1144,28 @@ def _format_timeframe(self, timeframe, delta): return form.format(delta) -class LevantArabicLocale(DefaultArabicLocale): +class LevantArabicLocale(ArabicLocale): names = ['ar_iq', 'ar_jo', 'ar_lb', 'ar_ps', 'ar_sy'] month_names = ['', 'كانون الثاني', 'شباط', 'آذار', 'نيسان', 'أيار', 'حزيران', 'تموز', 'آب', 'أيلول', 'تشرين الأول', 'تشرين الثاني', 'كانون الأول'] month_abbreviations = ['', 'كانون الثاني', 'شباط', 'آذار', 'نيسان', 'أيار', 'حزيران', 'تموز', 'آب', 'أيلول', 'تشرين الأول', 'تشرين الثاني', 'كانون الأول'] -class AlgeriaTunisiaArabicLocale(DefaultArabicLocale): +class AlgeriaTunisiaArabicLocale(ArabicLocale): names = ['ar_tn', 'ar_dz'] month_names = ['', 'جانفي', 'فيفري', 'مارس', 'أفريل', 'ماي', 'جوان', 'جويلية', 'أوت', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] month_abbreviations = ['', 'جانفي', 'فيفري', 'مارس', 'أفريل', 'ماي', 'جوان', 'جويلية', 'أوت', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] -class MauritaniaArabicLocale(DefaultArabicLocale): +class MauritaniaArabicLocale(ArabicLocale): names = ['ar_mr'] month_names = ['', 'يناير', 'فبراير', 'مارس', 'إبريل', 'مايو', 'يونيو', 'يوليو', 'أغشت', 'شتمبر', 'أكتوبر', 'نوفمبر', 'دجمبر'] month_abbreviations = ['', 'يناير', 'فبراير', 'مارس', 'إبريل', 'مايو', 'يونيو', 'يوليو', 'أغشت', 'شتمبر', 'أكتوبر', 'نوفمبر', 'دجمبر'] -class MoroccoArabicLocale(DefaultArabicLocale): +class MoroccoArabicLocale(ArabicLocale): names = ['ar_ma'] month_names = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'ماي', 'يونيو', 'يوليوز', 'غشت', 'شتنبر', 'أكتوبر', 'نونبر', 'دجنبر'] From 429483e6ee372dcc86ae288468a06496a2314eb4 Mon Sep 17 00:00:00 2001 From: Samer Atiani Date: Fri, 29 Sep 2017 11:50:54 -0400 Subject: [PATCH 111/649] Fix test --- tests/locales_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 2bb0d383d..a95e4b83b 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -559,7 +559,7 @@ class ArabicLocalesTest(Chai): def setUp(self): super(ArabicLocalesTest, self).setUp() - self.locale = locales.DefaultArabicLocale() + self.locale = locales.ArabicLocale() def test_timeframes(self): From 47a3ce47c6bc929508944122735d29d5cceb2d1f Mon Sep 17 00:00:00 2001 From: Renzo Date: Thu, 9 Nov 2017 14:34:57 +0800 Subject: [PATCH 112/649] Add tl_ph as TagalogLocale and en_ph as EnglishLocale --- arrow/locales.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 724a3d197..abf71d13f 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -186,7 +186,7 @@ def _format_relative(self, humanized, timeframe, delta): class EnglishLocale(Locale): - names = ['en', 'en_us', 'en_gb', 'en_au', 'en_be', 'en_jp', 'en_za', 'en_ca'] + names = ['en', 'en_us', 'en_gb', 'en_au', 'en_be', 'en_jp', 'en_za', 'en_ca', 'en_ph'] past = '{0} ago' future = 'in {0}' @@ -955,7 +955,7 @@ class BrazilianPortugueseLocale(PortugueseLocale): class TagalogLocale(Locale): - names = ['tl'] + names = ['tl', 'tl_ph'] past = 'nakaraang {0}' future = '{0} mula ngayon' From 8cda79463e88c6b156c46e91b371a663dfb393ca Mon Sep 17 00:00:00 2001 From: Renzo Date: Thu, 9 Nov 2017 14:54:50 +0800 Subject: [PATCH 113/649] Add ordinal numbers for Tagalog locale --- arrow/locales.py | 2 ++ tests/locales_tests.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index abf71d13f..a17825b9b 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -983,6 +983,8 @@ class TagalogLocale(Locale): day_names = ['', 'Lunes', 'Martes', 'Miyerkules', 'Huwebes', 'Biyernes', 'Sabado', 'Linggo'] day_abbreviations = ['', 'Lun', 'Mar', 'Miy', 'Huw', 'Biy', 'Sab', 'Lin'] + def _ordinal_number(self, n): + return 'ika-{0}'.format(n) class VietnameseLocale(Locale): diff --git a/tests/locales_tests.py b/tests/locales_tests.py index a95e4b83b..7d6d5b8e4 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -644,3 +644,22 @@ def test_format_relative_past(self): def test_format_relative_future(self): self.assertEqual(self.locale._format_relative('1 jam', 'hour', -1), '1 jam yang lalu') + + +class TagalogLocaleTests(Chai): + + def setUp(self): + super(TagalogLocaleTests, self).setUp() + + self.locale = locales.TagalogLocale() + + def test_ordinal_number(self): + assertEqual(self.locale.ordinal_number(0), 'ika-0') + assertEqual(self.locale.ordinal_number(1), 'ika-1') + assertEqual(self.locale.ordinal_number(2), 'ika-2') + assertEqual(self.locale.ordinal_number(3), 'ika-3') + assertEqual(self.locale.ordinal_number(10), 'ika-10') + assertEqual(self.locale.ordinal_number(23), 'ika-23') + assertEqual(self.locale.ordinal_number(100), 'ika-100') + assertEqual(self.locale.ordinal_number(103), 'ika-103') + assertEqual(self.locale.ordinal_number(114), 'ika-114') From 7904c53689f742bf61ce9e2598485cf7e6457cfb Mon Sep 17 00:00:00 2001 From: Renzo Date: Thu, 9 Nov 2017 15:32:49 +0800 Subject: [PATCH 114/649] Correct second/s timeframe for TagalogLocale --- arrow/locales.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index a17825b9b..dc77e5ad1 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -962,7 +962,8 @@ class TagalogLocale(Locale): timeframes = { 'now': 'ngayon lang', - 'seconds': 'segundo', + 'second': 'isang segundo', + 'seconds': '{0} segundo', 'minute': 'isang minuto', 'minutes': '{0} minuto', 'hour': 'isang oras', From 59c6a04c7ed877c1277fcb876227ae9cea8db759 Mon Sep 17 00:00:00 2001 From: Renzo Date: Thu, 9 Nov 2017 15:33:33 +0800 Subject: [PATCH 115/649] Create TagalogLocale tests --- tests/locales_tests.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 7d6d5b8e4..f5eea7a5e 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -653,6 +653,29 @@ def setUp(self): self.locale = locales.TagalogLocale() + def test_format_timeframe(self): + + assertEqual(self.locale._format_timeframe('second', 1), 'isang segundo') + assertEqual(self.locale._format_timeframe('minute', 1), 'isang minuto') + assertEqual(self.locale._format_timeframe('hour', 1), 'isang oras') + assertEqual(self.locale._format_timeframe('month', 1), 'isang buwan') + assertEqual(self.locale._format_timeframe('year', 1), 'isang taon') + + assertEqual(self.locale._format_timeframe('seconds', 2), '2 segundo') + assertEqual(self.locale._format_timeframe('minutes', 3), '3 minuto') + assertEqual(self.locale._format_timeframe('hours', 4), '4 oras') + assertEqual(self.locale._format_timeframe('months', 5), '5 buwan') + assertEqual(self.locale._format_timeframe('years', 6), '6 taon') + + def test_format_relative_now(self): + self.assertEqual(self.locale._format_relative('ngayon lang', 'now', 0), 'ngayon lang') + + def test_format_relative_past(self): + self.assertEqual(self.locale._format_relative('2 oras', 'hour', 2), '2 oras mula ngayon') + + def test_format_relative_future(self): + self.assertEqual(self.locale._format_relative('3 oras', 'hour', -3), 'nakaraang 3 oras') + def test_ordinal_number(self): assertEqual(self.locale.ordinal_number(0), 'ika-0') assertEqual(self.locale.ordinal_number(1), 'ika-1') From db226dae9d463c13d1211f322cd4e5d66dcfadc4 Mon Sep 17 00:00:00 2001 From: Renzo Date: Thu, 9 Nov 2017 15:39:04 +0800 Subject: [PATCH 116/649] Make seconds timeframe similar to EnglishLocale's --- arrow/locales.py | 3 +-- tests/locales_tests.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index dc77e5ad1..a17825b9b 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -962,8 +962,7 @@ class TagalogLocale(Locale): timeframes = { 'now': 'ngayon lang', - 'second': 'isang segundo', - 'seconds': '{0} segundo', + 'seconds': 'segundo', 'minute': 'isang minuto', 'minutes': '{0} minuto', 'hour': 'isang oras', diff --git a/tests/locales_tests.py b/tests/locales_tests.py index f5eea7a5e..7d548e7be 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -655,13 +655,12 @@ def setUp(self): def test_format_timeframe(self): - assertEqual(self.locale._format_timeframe('second', 1), 'isang segundo') assertEqual(self.locale._format_timeframe('minute', 1), 'isang minuto') assertEqual(self.locale._format_timeframe('hour', 1), 'isang oras') assertEqual(self.locale._format_timeframe('month', 1), 'isang buwan') assertEqual(self.locale._format_timeframe('year', 1), 'isang taon') - assertEqual(self.locale._format_timeframe('seconds', 2), '2 segundo') + assertEqual(self.locale._format_timeframe('seconds', 2), 'segundo') assertEqual(self.locale._format_timeframe('minutes', 3), '3 minuto') assertEqual(self.locale._format_timeframe('hours', 4), '4 oras') assertEqual(self.locale._format_timeframe('months', 5), '5 buwan') From 65d5d7439258242842b180654bee83e68c462bcd Mon Sep 17 00:00:00 2001 From: Skip Montanaro Date: Thu, 9 Nov 2017 05:20:16 -0600 Subject: [PATCH 117/649] include ZZZ in format example as a hint --- arrow/factory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow/factory.py b/arrow/factory.py index 6c33bbb26..ac204e4b2 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -112,8 +112,8 @@ def get(self, *args, **kwargs): **Two** arguments, both ``str``, to parse the first according to the format of the second:: - >>> arrow.get('2013-05-05 12:30:45', 'YYYY-MM-DD HH:mm:ss') - + >>> arrow.get('2013-05-05 12:30:45 America/Chicago', 'YYYY-MM-DD HH:mm:ss ZZZ') + **Two** arguments, first a ``str`` to parse and second a ``list`` of formats to try:: From c68499563cc22f08c15d67512b733393de975c00 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 24 Nov 2017 09:40:58 -0800 Subject: [PATCH 118/649] version bump to 0.11.0 --- arrow/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/__init__.py b/arrow/__init__.py index 63dd6be97..200fc13c5 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -4,5 +4,5 @@ from .factory import ArrowFactory from .api import get, now, utcnow -__version__ = '0.10.0' +__version__ = '0.11.0' VERSION = __version__ From a5b4f4c61e07c4acd38b93b374b26b9d9d63ee16 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 24 Nov 2017 09:46:45 -0800 Subject: [PATCH 119/649] update HISTORY.md --- HISTORY.md | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index e8d4dea43..79acfa106 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,15 @@ ## History +### 0.11.0 + +- [FIX] Fix grammar of ArabicLocale +- [NEW] Add Nepali Locale +- [FIX] Fix month name + rename AustriaLocale -> AustrianLocale +- [FIX] Fix typo in Basque Locale +- [FIX] Fix grammar in PortugueseBrazilian locale +- [FIX] Remove pip --user-mirrors flag +- [NEW] Add Indonesian Locale + ### 0.10.0 - [FIX] Fix getattr off by one for quarter @@ -17,9 +27,9 @@ - [FIX] respect limit even if end is defined range - [FIX] Separate replace & shift functions - [NEW] Added tox -- [FIX] Fix supported Python versions in documentation +- [FIX] Fix supported Python versions in documentation - [NEW] Azerbaijani locale added, locale issue fixed in Turkish. -- [FIX] Format ParserError's raise message +- [FIX] Format ParserError's raise message ### 0.8.0 @@ -30,13 +40,13 @@ - [NEW] Esperanto locale (batisteo) -### 0.7.0 +### 0.7.0 - [FIX] Parse localized strings #228 (swistakm) - [FIX] Modify tzinfo parameter in `get` api #221 (bottleimp) - [FIX] Fix Czech locale (PrehistoricTeam) - [FIX] Raise TypeError when adding/subtracting non-dates (itsmeolivia) - [FIX] Fix pytz conversion error (Kudo) -- [FIX] Fix overzealous time truncation in span_range (kdeldycke) +- [FIX] Fix overzealous time truncation in span_range (kdeldycke) - [NEW] Humanize for time duration #232 (ybrs) - [NEW] Add Thai locale (sipp11) - [NEW] Adding Belarusian (be) locale (oire) @@ -162,15 +172,15 @@ - [FIX] Locale day-of-week is no longer off by one (Cynddl) - [FIX] Corrected plurals of Ukrainian and Russian nouns (Catchagain) - [CHANGE] Old 0.1 ``arrow`` module method removed -- [CHANGE] Dropped timestamp support in ``range`` and ``span_range`` (never worked correctly) -- [CHANGE] Dropped parsing of single string as tz string in factory ``get`` method (replaced by ISO-8601) +- [CHANGE] Dropped timestamp support in ``range`` and ``span_range`` (never worked correctly) +- [CHANGE] Dropped parsing of single string as tz string in factory ``get`` method (replaced by ISO-8601) ### 0.3.5 - [NEW] French locale (Cynddl) - [NEW] Spanish locale (Slapresta) - [FIX] Ranges handle multiple timezones correctly (Ftobia) - + ### 0.3.4 - [FIX] Humanize no longer sometimes returns the wrong month delta @@ -182,7 +192,7 @@ - [NEW] Initial support for locale-based parsing and formatting - [NEW] ArrowFactory class, now proxied as the module API - [NEW] ``factory`` api method to obtain a factory for a custom type -- [FIX] Python 3 support and tests completely ironed out +- [FIX] Python 3 support and tests completely ironed out ### 0.3.2 @@ -212,7 +222,7 @@ - [NEW] Date parsing - [NEW] Date formatting - [NEW] ``floor``, ``ceil`` and ``span`` methods -- [NEW] ``datetime`` interface implementation +- [NEW] ``datetime`` interface implementation - [NEW] ``clone`` method - [NEW] ``get``, ``now`` and ``utcnow`` API methods From 62c53126a5a67dd4972135824c6d1c32d133824d Mon Sep 17 00:00:00 2001 From: Ramon Saraiva Date: Sat, 25 Nov 2017 14:09:34 -0200 Subject: [PATCH 120/649] Add functools-lru-cache to install_requires if version is less than 3 --- setup.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index a48aa912e..0fc079f30 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ import codecs import os.path import re +import sys try: from setuptools import setup @@ -25,6 +26,10 @@ def grep(attrname): file_text = read(fpath('arrow/__init__.py')) +install_requires = ['python-dateutil'] +if sys.version_info[0] < 3: + install_requires.append('backports.functools_lru_cache==1.2.1') + setup( name='arrow', version=grep('__version__'), @@ -36,9 +41,7 @@ def grep(attrname): license='Apache 2.0', packages=['arrow'], zip_safe=False, - install_requires=[ - 'python-dateutil' - ], + install_requires=install_requires, test_suite="tests", classifiers=[ 'Development Status :: 4 - Beta', From 66ac19380931e9bc0497aa5ecd0f34632f4e06cd Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sat, 25 Nov 2017 08:29:46 -0800 Subject: [PATCH 121/649] bump version, note in HISTORY.md --- HISTORY.md | 4 ++++ arrow/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 79acfa106..84e1e328f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,9 @@ ## History +### 0.12.0 + +- [FIX] Compatibility fix for Python 2.x + ### 0.11.0 - [FIX] Fix grammar of ArabicLocale diff --git a/arrow/__init__.py b/arrow/__init__.py index 200fc13c5..7a50b605a 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -4,5 +4,5 @@ from .factory import ArrowFactory from .api import get, now, utcnow -__version__ = '0.11.0' +__version__ = '0.12.0' VERSION = __version__ From 0f60d3ddf8d3ffe206f7e1038ce4c6f9831fbaf0 Mon Sep 17 00:00:00 2001 From: nishant jain Date: Sun, 10 Dec 2017 18:06:29 +0530 Subject: [PATCH 122/649] Makes the only_distance paramter value equivalent to value given to function when granularity paramter is also provided by user --- arrow/arrow.py | 2 +- tests/arrow_tests.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 7cd5dcb1f..1ef6e1469 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -806,7 +806,7 @@ def humanize(self, other=None, locale='en_us', only_distance=False, granularity= if(trunc(abs(delta)) != 1): granularity += 's' - return locale.describe(granularity, delta, only_distance=False) + return locale.describe(granularity, delta, only_distance=only_distance) # math def __add__(self, other): diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index e5a8b5223..6b7bf2e01 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1166,6 +1166,10 @@ def test_granularity(self): later108 = self.now.shift(seconds=10 ** 8) assertEqual(self.now.humanize(later108, granularity = 'year'), '3 years ago') assertEqual(later108.humanize(self.now, granularity = 'year'), 'in 3 years') + + later108onlydistance = self.now.shift(seconds=10 ** 8) + assertEqual(self.now.humanize(later108onlydistance , only_distance=True, granularity = 'year'), '3 years') + assertEqual(later108onlydistance .humanize(self.now, only_distance=True, granularity = 'year'), '3 years') with assertRaises(AttributeError): self.now.humanize(later108, granularity = 'years') From 61f4808d5f6bb116b5952b90b9c838e81a26e8b9 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Mon, 11 Dec 2017 12:00:54 -0700 Subject: [PATCH 123/649] Allow wheels to determine the required dependencies --- .gitignore | 1 + setup.py | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 56d9632ab..3276dce06 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ nosetests.xml local/ +build/ dist/ docs/_build/ .idea diff --git a/setup.py b/setup.py index 0fc079f30..933e122ad 100644 --- a/setup.py +++ b/setup.py @@ -26,10 +26,6 @@ def grep(attrname): file_text = read(fpath('arrow/__init__.py')) -install_requires = ['python-dateutil'] -if sys.version_info[0] < 3: - install_requires.append('backports.functools_lru_cache==1.2.1') - setup( name='arrow', version=grep('__version__'), @@ -41,7 +37,12 @@ def grep(attrname): license='Apache 2.0', packages=['arrow'], zip_safe=False, - install_requires=install_requires, + install_requires=[ + 'python-dateutil', + ], + extras_require={ + ":python_version=='2.7'": ['backports.functools_lru_cache>=1.2.1'], + }, test_suite="tests", classifiers=[ 'Development Status :: 4 - Beta', From 5a481c02babbbc458db7fe98d3cbaa8ad5dd50c7 Mon Sep 17 00:00:00 2001 From: Ramon Saraiva Date: Sun, 14 Jan 2018 22:46:46 -0200 Subject: [PATCH 124/649] Bump version --- arrow/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/__init__.py b/arrow/__init__.py index 7a50b605a..86ccb69f0 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -4,5 +4,5 @@ from .factory import ArrowFactory from .api import get, now, utcnow -__version__ = '0.12.0' +__version__ = '0.12.1' VERSION = __version__ From 874f2f99975cd9274bf8b316ee76e3a0ad6732cf Mon Sep 17 00:00:00 2001 From: Fergus Mitchell Date: Thu, 18 Jan 2018 18:19:57 +0000 Subject: [PATCH 125/649] get, now & utcnow will now use docs found in ArrowFactory --- arrow/api.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/arrow/api.py b/arrow/api.py index 83731458e..ce20aad25 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -8,12 +8,14 @@ from __future__ import absolute_import from arrow.factory import ArrowFactory +from functools import wraps # internal default factory. _factory = ArrowFactory() +@wraps(_factory.get) def get(*args, **kwargs): ''' Calls the default :class:`ArrowFactory ` ``get`` method. @@ -21,6 +23,8 @@ def get(*args, **kwargs): return _factory.get(*args, **kwargs) + +@wraps(_factory.utcnow) def utcnow(): ''' Calls the default :class:`ArrowFactory ` ``utcnow`` method. @@ -29,6 +33,7 @@ def utcnow(): return _factory.utcnow() +@wraps(_factory.now) def now(tz=None): ''' Calls the default :class:`ArrowFactory ` ``now`` method. From c03ff6add9a4b562a72a285a3834c8285bf6aded Mon Sep 17 00:00:00 2001 From: Fergus Mitchell Date: Mon, 29 Jan 2018 16:18:39 +0000 Subject: [PATCH 126/649] Changed from using functools.wraps to reassigning .__doc__ --- arrow/api.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/arrow/api.py b/arrow/api.py index ce20aad25..f23f27398 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -8,14 +8,12 @@ from __future__ import absolute_import from arrow.factory import ArrowFactory -from functools import wraps # internal default factory. _factory = ArrowFactory() -@wraps(_factory.get) def get(*args, **kwargs): ''' Calls the default :class:`ArrowFactory ` ``get`` method. @@ -23,8 +21,9 @@ def get(*args, **kwargs): return _factory.get(*args, **kwargs) +get.__doc__ = _factory.get.__doc__ + -@wraps(_factory.utcnow) def utcnow(): ''' Calls the default :class:`ArrowFactory ` ``utcnow`` method. @@ -32,8 +31,9 @@ def utcnow(): return _factory.utcnow() +utcnow.__doc__ = _factory.utcnow.__doc__ + -@wraps(_factory.now) def now(tz=None): ''' Calls the default :class:`ArrowFactory ` ``now`` method. @@ -41,6 +41,8 @@ def now(tz=None): return _factory.now(tz) +now.__doc__ = _factory.now.__doc__ + def factory(type): ''' Returns an :class:`.ArrowFactory` for the specified :class:`Arrow ` @@ -52,6 +54,4 @@ def factory(type): return ArrowFactory(type) - __all__ = ['get', 'utcnow', 'now', 'factory'] - From 5d9136742f1fcbccd757744c2a21f6b0f85fac46 Mon Sep 17 00:00:00 2001 From: Karl Kroening Date: Mon, 29 Jan 2018 18:11:11 -0800 Subject: [PATCH 127/649] #515, #462, #493: allow arrow.get() to be called with str+tzinfo parameter combo --- arrow/factory.py | 2 +- tests/factory_tests.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/arrow/factory.py b/arrow/factory.py index 6c33bbb26..29a7033c9 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -172,7 +172,7 @@ def get(self, *args, **kwargs): # (str) -> parse. elif isstr(arg): dt = parser.DateTimeParser(locale).parse_iso(arg) - return self.type.fromdatetime(dt) + return self.type.fromdatetime(dt, tz) # (struct_time) -> from struct_time elif isinstance(arg, struct_time): diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 6b5395765..378c0f51c 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -149,6 +149,12 @@ def test_two_args_str_str(self): assertEqual(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')) + + assertDtEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz('US/Pacific'))) + def test_two_args_twitter_format(self): # format returned by twitter API for created_at: From f06c13ee9394a31aa5a52dca3b4d4106bee475c5 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Thu, 9 Nov 2017 08:58:21 -0500 Subject: [PATCH 128/649] Drop datetime.utcnow in favor of datetime.now(UTC) --- arrow/arrow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 1ef6e1469..94c643a43 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -79,8 +79,8 @@ def now(cls, tzinfo=None): ''' - utc = datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc()) - dt = utc.astimezone(dateutil_tz.tzlocal() if tzinfo is None else tzinfo) + tzinfo = tzinfo or dateutil_tz.tzlocal() + dt = datetime.now(tzinfo) return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond, dt.tzinfo) @@ -92,10 +92,10 @@ def utcnow(cls): ''' - dt = datetime.utcnow() + dt = datetime.now(dateutil_tz.tzutc()) return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, dateutil_tz.tzutc()) + dt.microsecond, dt.tzinfo) @classmethod def fromtimestamp(cls, timestamp, tzinfo=None): From 72a275e0306f082f8d5e04ca14d400a400801954 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Fri, 2 Feb 2018 08:42:38 -0500 Subject: [PATCH 129/649] Respect falsy tzinfos when falling back --- arrow/arrow.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 94c643a43..de880bbda 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -62,7 +62,7 @@ def __init__(self, year, month, day, hour=0, minute=0, second=0, microsecond=0, if util.isstr(tzinfo): tzinfo = parser.TzinfoParser.parse(tzinfo) - tzinfo = tzinfo or dateutil_tz.tzutc() + tzinfo = tzinfo if tzinfo is not None else dateutil_tz.tzutc() self._datetime = datetime(year, month, day, hour, minute, second, microsecond, tzinfo) @@ -79,7 +79,7 @@ def now(cls, tzinfo=None): ''' - tzinfo = tzinfo or dateutil_tz.tzlocal() + tzinfo = tzinfo if tzinfo is not None else dateutil_tz.tzlocal() dt = datetime.now(tzinfo) return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, @@ -112,7 +112,7 @@ def fromtimestamp(cls, timestamp, tzinfo=None): ''' - tzinfo = tzinfo or dateutil_tz.tzlocal() + tzinfo = tzinfo if tzinfo is not None else dateutil_tz.tzlocal() timestamp = cls._get_timestamp_from_input(timestamp) dt = datetime.fromtimestamp(timestamp, tzinfo) @@ -151,7 +151,11 @@ def fromdatetime(cls, dt, tzinfo=None): ''' - tzinfo = tzinfo or dt.tzinfo or dateutil_tz.tzutc() + if tzinfo is None: + if dt.tzinfo is None: + tzinfo = dateutil_tz.tzutc() + else: + tzinfo = dt.tzinfo return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond, tzinfo) @@ -165,7 +169,7 @@ def fromdate(cls, date, tzinfo=None): :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to UTC. ''' - tzinfo = tzinfo or dateutil_tz.tzutc() + tzinfo = tzinfo if tzinfo is not None else dateutil_tz.tzutc() return cls(date.year, date.month, date.day, tzinfo=tzinfo) @@ -182,10 +186,10 @@ def strptime(cls, date_str, fmt, tzinfo=None): ''' dt = datetime.strptime(date_str, fmt) - tzinfo = tzinfo or dt.tzinfo + tzinfo = tzinfo if tzinfo is not None else dt.tzinfo return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, tzinfo) + dt.microsecond, tzinfo) # factories: ranges and spans From 64301a7d18469ac77c8f0f8b21ad3ea2a5db1395 Mon Sep 17 00:00:00 2001 From: zcribe Date: Wed, 21 Feb 2018 19:29:14 +0200 Subject: [PATCH 130/649] Added Estonian localisation Adapted version of hungarian locale. --- arrow/locales.py | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index a17825b9b..17b962bd6 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2157,5 +2157,44 @@ class NepaliLocale(Locale): '', 'सोम', 'मंगल', 'बुध', 'बिहि', 'शुक्र', 'शनि', 'आइत' ] - + class EstonianLocale(Locale): + names = ['ee', 'et'] + + past = '{0} tagasi' + future = '{0} pärast' + + timeframes = { + 'now': {'past': 'just nüüd', 'future': 'just nüüd'}, + 'seconds': {'past': 'sekundit', 'future': 'sekundi'}, + 'minute': {'past': 'üks minut', 'future': 'ühe minuti'}, + 'minutes': {'past': '{0} minutit', 'future': '{0} minuti'}, + 'hour': {'past': 'tund aega', 'future': 'tunni aja'}, + 'hours': {'past': '{0} tundi', 'future': '{0} tunni'}, + 'day': {'past': 'üks päev', 'future': 'ühe päeva'}, + 'days': {'past': '{0} päeva', 'future': '{0} päeva'}, + 'month': {'past': 'üks kuu', 'future': 'ühe kuu'}, + 'months': {'past': '{0} kuud', 'future': '{0}kuu'}, + 'year': {'past': 'aasta', 'future': 'ühe aasta'}, + 'years': {'past': '{0} aastat', 'future': '{0} aasta'} + } + + month_names = ['', 'Jaanuar', 'Veebruar', 'Märts', 'Aprill', 'Mai', 'Juuni', 'Juuli', + 'August', 'September', 'Oktoober', 'November', 'Detsember'] + month_abbreviations = ['', 'Jan', 'Veb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', + 'Sep', 'Okt', 'Nov', 'Dets'] + + day_names = ['', 'Esmaspäev', 'Teisipäev', 'Kolmapäev', 'Neljapäev', 'Reede', 'Laupäev', 'Pühapäev'] + day_abbreviations = ['', 'Esm', 'Teis', 'Kolm', 'Nelj', 'Re', 'Lau', 'Püh'] + + def _format_timeframe(self, timeframe, delta): + form = self.timeframes[timeframe] + + if isinstance(form,dict): + if delta > 0: + form = form['future'] + else: + form = form['past'] + return form.format(abs(delta)) + + _locales = _map_locales() From 620fe628cab97188f4c6ab6b812dd568bb1e9fd1 Mon Sep 17 00:00:00 2001 From: zcribe Date: Wed, 21 Feb 2018 19:37:23 +0200 Subject: [PATCH 131/649] Update locales.py --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index 17b962bd6..f0c05f05d 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2157,7 +2157,7 @@ class NepaliLocale(Locale): '', 'सोम', 'मंगल', 'बुध', 'बिहि', 'शुक्र', 'शनि', 'आइत' ] - class EstonianLocale(Locale): +class EstonianLocale(Locale): names = ['ee', 'et'] past = '{0} tagasi' From 1664c3c16faa589127d3cf3b132e2469b3ab6b87 Mon Sep 17 00:00:00 2001 From: zcribe Date: Wed, 21 Feb 2018 19:51:10 +0200 Subject: [PATCH 132/649] Added Estonian locale tests --- tests/locales_tests.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 7d548e7be..edcaab35f 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -685,3 +685,17 @@ def test_ordinal_number(self): assertEqual(self.locale.ordinal_number(100), 'ika-100') assertEqual(self.locale.ordinal_number(103), 'ika-103') assertEqual(self.locale.ordinal_number(114), 'ika-114') + + +class EstonianLocaleTests(Chai): + + def setUp(self): + super(EstonianLocaleTests, self).setUp() + + self.locale = locales.EstonianLocale() + + def test_format_timeframe(self): + assertEqual(self.locale._format_timeframe('hours', 2), '2 tunni') + assertEqual(self.locale._format_timeframe('hour', 0), 'tund aega') + assertEqual(self.locale._format_timeframe('hours', -2), '2 tundi') + assertEqual(self.locale._format_timeframe('now', 0), 'just nüüd') From 0f92d43e995df37a79d84e79516a1006ad6596dd Mon Sep 17 00:00:00 2001 From: zcribe Date: Sat, 24 Feb 2018 19:37:27 +0200 Subject: [PATCH 133/649] Added singular second and fixed typos --- arrow/locales.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index f0c05f05d..5a3b1cd74 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2165,7 +2165,8 @@ class EstonianLocale(Locale): timeframes = { 'now': {'past': 'just nüüd', 'future': 'just nüüd'}, - 'seconds': {'past': 'sekundit', 'future': 'sekundi'}, + 'second': {'past': 'üks sekund', 'future': 'ühe sekundi'}, + 'seconds': {'past': '{0} sekundit', 'future': '{0} sekundi'}, 'minute': {'past': 'üks minut', 'future': 'ühe minuti'}, 'minutes': {'past': '{0} minutit', 'future': '{0} minuti'}, 'hour': {'past': 'tund aega', 'future': 'tunni aja'}, @@ -2173,8 +2174,8 @@ class EstonianLocale(Locale): 'day': {'past': 'üks päev', 'future': 'ühe päeva'}, 'days': {'past': '{0} päeva', 'future': '{0} päeva'}, 'month': {'past': 'üks kuu', 'future': 'ühe kuu'}, - 'months': {'past': '{0} kuud', 'future': '{0}kuu'}, - 'year': {'past': 'aasta', 'future': 'ühe aasta'}, + 'months': {'past': '{0} kuud', 'future': '{0} kuu'}, + 'year': {'past': 'üks aasta', 'future': 'ühe aasta'}, 'years': {'past': '{0} aastat', 'future': '{0} aasta'} } From 9542ce950aa686efcb85705d1a47484494b63886 Mon Sep 17 00:00:00 2001 From: zcribe Date: Sat, 24 Feb 2018 19:38:38 +0200 Subject: [PATCH 134/649] Added tests. --- tests/locales_tests.py | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/tests/locales_tests.py b/tests/locales_tests.py index edcaab35f..e405d1d33 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -695,7 +695,42 @@ def setUp(self): self.locale = locales.EstonianLocale() def test_format_timeframe(self): - assertEqual(self.locale._format_timeframe('hours', 2), '2 tunni') - assertEqual(self.locale._format_timeframe('hour', 0), 'tund aega') - assertEqual(self.locale._format_timeframe('hours', -2), '2 tundi') assertEqual(self.locale._format_timeframe('now', 0), 'just nüüd') + assertEqual(self.locale._format_timeframe('second', 1), 'ühe sekundi') + assertEqual(self.locale._format_timeframe('seconds', 3), '3 sekundi') + assertEqual(self.locale._format_timeframe('seconds', 30), '30 sekundi') + assertEqual(self.locale._format_timeframe('minute', 1), 'ühe minuti') + assertEqual(self.locale._format_timeframe('minutes', 4), '4 minuti') + assertEqual(self.locale._format_timeframe('minutes', 40), '40 minuti') + assertEqual(self.locale._format_timeframe('hour', 1), 'tunni aja') + assertEqual(self.locale._format_timeframe('hours', 5), '5 tunni') + assertEqual(self.locale._format_timeframe('hours', 23), '23 tunni') + assertEqual(self.locale._format_timeframe('day', 1), 'ühe päeva') + assertEqual(self.locale._format_timeframe('days', 6), '6 päeva') + assertEqual(self.locale._format_timeframe('days', 12), '12 päeva') + assertEqual(self.locale._format_timeframe('month', 1), 'ühe kuu') + assertEqual(self.locale._format_timeframe('months', 7), '7 kuu') + assertEqual(self.locale._format_timeframe('months', 11), '11 kuu') + assertEqual(self.locale._format_timeframe('year', 1), 'ühe aasta') + assertEqual(self.locale._format_timeframe('years', 8), '8 aasta') + assertEqual(self.locale._format_timeframe('years', 12), '12 aasta') + + assertEqual(self.locale._format_timeframe('now', 0), 'just nüüd') + assertEqual(self.locale._format_timeframe('second', -1), 'üks sekund') + assertEqual(self.locale._format_timeframe('seconds', -9), '9 sekundit') + assertEqual(self.locale._format_timeframe('seconds', -12), '12 sekundit') + assertEqual(self.locale._format_timeframe('minute', -1), 'ühe minuti') + assertEqual(self.locale._format_timeframe('minutes', -2), '2 minutit') + assertEqual(self.locale._format_timeframe('minutes', -10), '10 minutit') + assertEqual(self.locale._format_timeframe('hour', -1), 'tund aega') + assertEqual(self.locale._format_timeframe('hours', -3), '3 tundi') + assertEqual(self.locale._format_timeframe('hours', -11), '11 tundi') + assertEqual(self.locale._format_timeframe('day', -1), 'üks päev') + assertEqual(self.locale._format_timeframe('days', -2), '2 päeva') + assertEqual(self.locale._format_timeframe('days', -12), '12 päeva') + assertEqual(self.locale._format_timeframe('month', -1), 'üks kuu') + assertEqual(self.locale._format_timeframe('months', -3), '3 kuud') + assertEqual(self.locale._format_timeframe('months', -13), '13 kuud') + assertEqual(self.locale._format_timeframe('year', -1), 'üks aasta') + assertEqual(self.locale._format_timeframe('years', -4), '4 aastat') + assertEqual(self.locale._format_timeframe('years', -14), '14 aastat') From c113e3eefb14ad8e8d421919946bb135ea997b27 Mon Sep 17 00:00:00 2001 From: zcribe Date: Sat, 24 Feb 2018 19:43:10 +0200 Subject: [PATCH 135/649] Removed extra spaces. --- tests/locales_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/locales_tests.py b/tests/locales_tests.py index e405d1d33..1cea907e4 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -717,8 +717,8 @@ def test_format_timeframe(self): assertEqual(self.locale._format_timeframe('now', 0), 'just nüüd') assertEqual(self.locale._format_timeframe('second', -1), 'üks sekund') - assertEqual(self.locale._format_timeframe('seconds', -9), '9 sekundit') - assertEqual(self.locale._format_timeframe('seconds', -12), '12 sekundit') + assertEqual(self.locale._format_timeframe('seconds', -9), '9 sekundit') + assertEqual(self.locale._format_timeframe('seconds', -12), '12 sekundit') assertEqual(self.locale._format_timeframe('minute', -1), 'ühe minuti') assertEqual(self.locale._format_timeframe('minutes', -2), '2 minutit') assertEqual(self.locale._format_timeframe('minutes', -10), '10 minutit') From aeb35d87bd1cddf47a41401201d90355289e34ed Mon Sep 17 00:00:00 2001 From: zcribe Date: Sat, 24 Feb 2018 19:56:45 +0200 Subject: [PATCH 136/649] Fixed bad tests --- tests/locales_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 1cea907e4..9afe6bcfe 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -719,7 +719,7 @@ def test_format_timeframe(self): assertEqual(self.locale._format_timeframe('second', -1), 'üks sekund') assertEqual(self.locale._format_timeframe('seconds', -9), '9 sekundit') assertEqual(self.locale._format_timeframe('seconds', -12), '12 sekundit') - assertEqual(self.locale._format_timeframe('minute', -1), 'ühe minuti') + assertEqual(self.locale._format_timeframe('minute', -1), 'üks minut') assertEqual(self.locale._format_timeframe('minutes', -2), '2 minutit') assertEqual(self.locale._format_timeframe('minutes', -10), '10 minutit') assertEqual(self.locale._format_timeframe('hour', -1), 'tund aega') From 73ce2aa97be9dc092ec76cd4f9109788e6bc8cec Mon Sep 17 00:00:00 2001 From: zcribe Date: Thu, 8 Mar 2018 17:26:43 +0200 Subject: [PATCH 137/649] Removed 'isInstance' check --- arrow/locales.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 5a3b1cd74..35c686e29 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2189,12 +2189,10 @@ class EstonianLocale(Locale): def _format_timeframe(self, timeframe, delta): form = self.timeframes[timeframe] - - if isinstance(form,dict): - if delta > 0: - form = form['future'] - else: - form = form['past'] + if delta > 0: + form = form['future'] + else: + form = form['past'] return form.format(abs(delta)) From 3f67c8be2cff3e9f634a31e4a48a2c1bf4e0eaa0 Mon Sep 17 00:00:00 2001 From: Srdjan Grubor Date: Mon, 22 Jan 2018 16:08:34 -0600 Subject: [PATCH 138/649] Made format string handle RegEx chars properly Old code did not escape RegEx special characters properly so now me make sure that the source string is first properly escaped. --- arrow/parser.py | 7 +++++-- tests/parser_tests.py | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index e4f368ad5..df8cc1a47 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -127,8 +127,11 @@ def _generate_pattern_re(self, fmt): tokens = [] offset = 0 + # Escape all special RegEx chars + escaped_fmt = re.escape(fmt) + # Extract the bracketed expressions to be reinserted later. - escaped_fmt = re.sub(self._ESCAPE_RE, "#", fmt) + escaped_fmt = re.sub(self._ESCAPE_RE, "#", escaped_fmt) # Any number of S is the same as one. escaped_fmt = re.sub('S+', 'S', escaped_fmt) escaped_data = re.findall(self._ESCAPE_RE, fmt) @@ -151,7 +154,7 @@ def _generate_pattern_re(self, fmt): offset += len(input_pattern) - (m.end() - m.start()) final_fmt_pattern = "" - a = fmt_pattern.split("#") + a = fmt_pattern.split("\#") b = escaped_data # Due to the way Python splits, 'a' will always be longer diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 7682df8c3..a179be4e6 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -836,3 +836,9 @@ def test_escape(self): assertEqual( self.parser.parse("I'm entirely escaped, weee!", format), datetime(1, 1, 1)) + + # Special RegEx characters + format = 'MMM DD, YYYY |^${}().*+?<>-& h:mm A' + assertEqual( + self.parser.parse("Dec 31, 2017 |^${}().*+?<>-& 2:00 AM", format), + datetime(2017, 12, 31, 2, 0)) From 7437f2d72caee9d74674d1b6adcf904165356978 Mon Sep 17 00:00:00 2001 From: "Justin Wood (Callek)" Date: Tue, 27 Mar 2018 08:38:08 -0400 Subject: [PATCH 139/649] Fix spacing in deprecation warning I was fixing some deprecation warnings in code I use, and came across this formatting error. (reads "valueis") --- arrow/arrow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 72b412cf8..c8e260666 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -486,7 +486,7 @@ def replace(self, **kwargs): absolute_kwargs[key] = value elif key in self._ATTRS_PLURAL or key in ['weeks', 'quarters']: # TODO: DEPRECATED - warnings.warn("replace() with plural property to shift value" + warnings.warn("replace() with plural property to shift value " "is deprecated, use shift() instead", DeprecationWarning) relative_kwargs[key] = value From d998cb7d18c881045a96ec3b5565e0c469bc6d06 Mon Sep 17 00:00:00 2001 From: Rafael Sierra Date: Thu, 13 Dec 2018 13:27:46 +0100 Subject: [PATCH 140/649] Added support for ZZZ while formatting (#391) --- arrow/formatter.py | 5 ++++- tests/formatter_tests.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/arrow/formatter.py b/arrow/formatter.py index 50fd3a179..cbdffcbf4 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -9,7 +9,7 @@ class DateTimeFormatter(object): - _FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?|a|A|X)') + _FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X)') def __init__(self, locale='en_us'): @@ -89,6 +89,9 @@ def _format_token(self, dt, token): if token == 'X': return str(calendar.timegm(dt.utctimetuple())) + if token == 'ZZZ': + return dt.tzname() + if token in ['ZZ', 'Z']: separator = ':' if token == 'ZZ' else '' tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo diff --git a/tests/formatter_tests.py b/tests/formatter_tests.py index 23a977c6e..a6b1ca887 100644 --- a/tests/formatter_tests.py +++ b/tests/formatter_tests.py @@ -4,6 +4,7 @@ from datetime import datetime from dateutil import tz as dateutil_tz +import pytz import time class DateTimeFormatterFormatTokenTests(Chai): @@ -118,6 +119,21 @@ def test_timezone(self): result = self.formatter._format_token(dt, 'Z') assertTrue(result == '-0700' or result == '-0800') + def test_timezone_formatter(self): + + tz_map = { + 'BRST': 'America/Sao_Paulo', + 'CET': 'Europe/Berlin', + 'JST': 'Asia/Tokyo', + 'PST': 'US/Pacific', + } + + for abbreviation, full_name in tz_map.items(): + # 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_name)) + result = self.formatter._format_token(dt, 'ZZZ') + assertEqual(result, abbreviation) + def test_am_pm(self): dt = datetime(2012, 1, 1, 11) From a32f16337bc3fde3024b6d392b7074b3ab73657f Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Thu, 13 Dec 2018 16:27:32 +0000 Subject: [PATCH 141/649] Update HISTORY.md with 0.12.1 (#553) * Update HISTORY.md with 0.12.1 * Temporarily disable failing formatter test --- HISTORY.md | 6 +++++- tests/formatter_tests.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 84e1e328f..39562bcff 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ ## History +### 0.12.1 + +- [FIX] Allow universal wheels to be generated and reliably installed. +- [FIX] Make humanize respect only_distance when granularity argument is also given. + ### 0.12.0 - [FIX] Compatibility fix for Python 2.x @@ -242,4 +247,3 @@ - **Started tracking changes** - [NEW] Parsing of ISO-formatted time zone offsets (e.g. '+02:30', '-05:00') - [NEW] Resolved some issues with timestamps and delta / Olson time zones - diff --git a/tests/formatter_tests.py b/tests/formatter_tests.py index a6b1ca887..a36039092 100644 --- a/tests/formatter_tests.py +++ b/tests/formatter_tests.py @@ -122,7 +122,7 @@ def test_timezone(self): def test_timezone_formatter(self): tz_map = { - 'BRST': 'America/Sao_Paulo', + #'BRST': 'America/Sao_Paulo', TODO investigate why this fails 'CET': 'Europe/Berlin', 'JST': 'Asia/Tokyo', 'PST': 'US/Pacific', From 918bb42169bd8a487d05243811d1344f6af75845 Mon Sep 17 00:00:00 2001 From: Charalambos Poulikidis Date: Sun, 16 Dec 2018 21:28:59 +0200 Subject: [PATCH 142/649] Update locales.py (#555) Fixed some accents for greek locale. According to this (greek) article https://e-didaskalia.blogspot.com/2015/05/blog-post_58.html. --- arrow/locales.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 35c686e29..17113b8d2 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -351,9 +351,9 @@ class GreekLocale(Locale): 'seconds': 'δευτερόλεπτα', 'minute': 'ένα λεπτό', 'minutes': '{0} λεπτά', - 'hour': 'μια ώρα', + 'hour': 'μία ώρα', 'hours': '{0} ώρες', - 'day': 'μια μέρα', + 'day': 'μία μέρα', 'days': '{0} μέρες', 'month': 'ένα μήνα', 'months': '{0} μήνες', From f008ac7c2e64d498f493318f6c6df8a56a7926dc Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 20 Dec 2018 12:18:08 +0200 Subject: [PATCH 143/649] Drop support for EOL Python 2.6 and 3.3, add support for 3.6 (#472) * Drop support for EOL Python 2.6 * Update string formatters * Add support for Python 3.6 * Add python_requires for pip to install correct release for user's Python version * Fix version check * Drop support for EOL Python 3.3 * Upgrade Python syntax with pyupgrade --- .travis.yml | 6 ++---- Makefile | 14 ++++--------- README.rst | 2 +- arrow/arrow.py | 20 +++++++++--------- arrow/factory.py | 8 ++++---- arrow/formatter.py | 26 ++++++++++++------------ arrow/locales.py | 48 ++++++++++++++++++++++---------------------- arrow/parser.py | 14 ++++++------- arrow/util.py | 25 ++++++----------------- docs/index.rst | 2 +- requirements26.txt | 1 - setup.py | 8 +++----- tests/arrow_tests.py | 14 ++++++------- tests/util_tests.py | 28 -------------------------- tox.ini | 2 +- 15 files changed, 83 insertions(+), 135 deletions(-) delete mode 100644 requirements26.txt delete mode 100644 tests/util_tests.py diff --git a/.travis.yml b/.travis.yml index 57d58ba67..713403dc8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,15 @@ sudo: false language: python python: - - 2.6 - 2.7 - - 3.3 - 3.4 - 3.5 + - 3.6 install: - - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then make build26; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then make build27; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]]; then make build33; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then make build34; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then make build35; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then make build36; fi - pip install codecov script: make test after_success: codecov diff --git a/Makefile b/Makefile index e1a3b2cf5..adabbd6ef 100644 --- a/Makefile +++ b/Makefile @@ -6,24 +6,18 @@ build27: virtualenv local --python=python2.7 local/bin/pip install -r requirements.txt -build26: - virtualenv local --python=python2.6 - local/bin/pip install -r requirements.txt - local/bin/pip install -r requirements26.txt - -build33: - virtualenv local --python=python3.3 - local/bin/pip install -r requirements.txt - build34: virtualenv local --python=python3.4 local/bin/pip install -r requirements.txt - build35: virtualenv local --python=python3.5 local/bin/pip install -r requirements.txt +build36: + virtualenv local --python=python3.6 + local/bin/pip install -r requirements.txt + test: rm -f .coverage . local/bin/activate && nosetests diff --git a/README.rst b/README.rst index 2c3ffef5e..125a37165 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ Features -------- - Fully implemented, drop-in replacement for datetime -- Supports Python 2.6, 2.7, 3.3, 3.4 and 3.5 +- Supports Python 2.7, 3.4, 3.5 and 3.6 - Time zone-aware & UTC by default - Provides super-simple creation options for many common input scenarios - Updated .replace method with support for relative offsets, including weeks diff --git a/arrow/arrow.py b/arrow/arrow.py index c8e260666..dedf90711 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -54,7 +54,7 @@ class Arrow(object): resolution = datetime.resolution _ATTRS = ['year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond'] - _ATTRS_PLURAL = ['{0}s'.format(a) for a in _ATTRS] + _ATTRS_PLURAL = ['{}s'.format(a) for a in _ATTRS] _MONTHS_PER_QUARTER = 3 def __init__(self, year, month, day, hour=0, minute=0, second=0, microsecond=0, @@ -368,7 +368,7 @@ def interval(cls, frame, start, end, interval=1, tz=None): # representations def __repr__(self): - return '<{0} [{1}]>'.format(self.__class__.__name__, self.__str__()) + return '<{} [{}]>'.format(self.__class__.__name__, self.__str__()) def __str__(self): return self._datetime.isoformat() @@ -491,9 +491,9 @@ def replace(self, **kwargs): DeprecationWarning) relative_kwargs[key] = value elif key in ['week', 'quarter']: - raise AttributeError('setting absolute {0} is not supported'.format(key)) + raise AttributeError('setting absolute {} is not supported'.format(key)) elif key !='tzinfo': - raise AttributeError('unknown attribute: "{0}"'.format(key)) + raise AttributeError('unknown attribute: "{}"'.format(key)) # core datetime does not support quarters, translate to months. relative_kwargs.setdefault('months', 0) @@ -895,7 +895,7 @@ def __le__(self, other): def __cmp__(self, other): if sys.version_info[0] < 3: # pragma: no cover if not isinstance(other, (Arrow, datetime)): - raise TypeError('can\'t compare \'{0}\' to \'{1}\''.format( + raise TypeError('can\'t compare \'{}\' to \'{}\''.format( type(self), type(other))) @@ -1005,7 +1005,7 @@ def _get_tzinfo(tz_expr): try: return parser.TzinfoParser.parse(tz_expr) except parser.ParserError: - raise ValueError('\'{0}\' not recognized as a timezone'.format( + raise ValueError('\'{}\' not recognized as a timezone'.format( tz_expr)) @classmethod @@ -1022,13 +1022,13 @@ def _get_datetime(cls, expr): return cls.utcfromtimestamp(expr).datetime except: raise ValueError( - '\'{0}\' not recognized as a timestamp or datetime'.format(expr)) + '\'{}\' not recognized as a timestamp or datetime'.format(expr)) @classmethod def _get_frames(cls, name): if name in cls._ATTRS: - return name, '{0}s'.format(name), 1 + return name, '{}s'.format(name), 1 elif name in ['week', 'weeks']: return 'week', 'weeks', 1 @@ -1036,7 +1036,7 @@ def _get_frames(cls, name): return 'quarter', 'months', 3 supported = ', '.join(cls._ATTRS + ['week', 'weeks'] + ['quarter', 'quarters']) - raise AttributeError('range/span over frame {0} not supported. Supported frames: {1}'.format(name, supported)) + raise AttributeError('range/span over frame {} not supported. Supported frames: {}'.format(name, supported)) @classmethod def _get_iteration_params(cls, end, limit): @@ -1059,7 +1059,7 @@ def _get_timestamp_from_input(timestamp): try: return float(timestamp) except: - raise ValueError('cannot parse \'{0}\' as a timestamp'.format(timestamp)) + raise ValueError('cannot parse \'{}\' as a timestamp'.format(timestamp)) Arrow.min = Arrow.fromdatetime(datetime.min) Arrow.max = Arrow.fromdatetime(datetime.max) diff --git a/arrow/factory.py b/arrow/factory.py index 9571afdcb..535dbc57c 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -179,7 +179,7 @@ def get(self, *args, **kwargs): return self.type.utcfromtimestamp(calendar.timegm(arg)) else: - raise TypeError('Can\'t parse single argument type of \'{0}\''.format(type(arg))) + raise TypeError('Can\'t parse single argument type of \'{}\''.format(type(arg))) elif arg_count == 2: @@ -191,7 +191,7 @@ def get(self, *args, **kwargs): if isinstance(arg_2, tzinfo) or isstr(arg_2): return self.type.fromdatetime(arg_1, arg_2) else: - raise TypeError('Can\'t parse two arguments of types \'datetime\', \'{0}\''.format( + raise TypeError('Can\'t parse two arguments of types \'datetime\', \'{}\''.format( type(arg_2))) elif isinstance(arg_1, date): @@ -200,7 +200,7 @@ def get(self, *args, **kwargs): if isinstance(arg_2, tzinfo) or isstr(arg_2): return self.type.fromdate(arg_1, tzinfo=arg_2) else: - raise TypeError('Can\'t parse two arguments of types \'date\', \'{0}\''.format( + raise TypeError('Can\'t parse two arguments of types \'date\', \'{}\''.format( type(arg_2))) # (str, format) -> parse. @@ -209,7 +209,7 @@ def get(self, *args, **kwargs): return self.type.fromdatetime(dt, tzinfo=tz) else: - raise TypeError('Can\'t parse two arguments of types \'{0}\', \'{1}\''.format( + raise TypeError('Can\'t parse two arguments of types \'{}\', \'{}\''.format( type(arg_1), type(arg_2))) # 3+ args -> datetime-like via constructor. diff --git a/arrow/formatter.py b/arrow/formatter.py index cbdffcbf4..5194c356b 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -31,16 +31,16 @@ def _format_token(self, dt, token): if token == 'MMM': return self.locale.month_abbreviation(dt.month) if token == 'MM': - return '{0:02d}'.format(dt.month) + return '{:02d}'.format(dt.month) if token == 'M': return str(dt.month) if token == 'DDDD': - return '{0:03d}'.format(dt.timetuple().tm_yday) + return '{:03d}'.format(dt.timetuple().tm_yday) if token == 'DDD': return str(dt.timetuple().tm_yday) if token == 'DD': - return '{0:02d}'.format(dt.day) + return '{:02d}'.format(dt.day) if token == 'D': return str(dt.day) @@ -55,34 +55,34 @@ def _format_token(self, dt, token): return str(dt.isoweekday()) if token == 'HH': - return '{0:02d}'.format(dt.hour) + return '{:02d}'.format(dt.hour) if token == 'H': return str(dt.hour) if token == 'hh': - return '{0:02d}'.format(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) + return '{:02d}'.format(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) if token == 'h': return str(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) if token == 'mm': - return '{0:02d}'.format(dt.minute) + return '{:02d}'.format(dt.minute) if token == 'm': return str(dt.minute) if token == 'ss': - return '{0:02d}'.format(dt.second) + return '{:02d}'.format(dt.second) if token == 's': return str(dt.second) if token == 'SSSSSS': - return str('{0:06d}'.format(int(dt.microsecond))) + return str('{:06d}'.format(int(dt.microsecond))) if token == 'SSSSS': - return str('{0:05d}'.format(int(dt.microsecond / 10))) + return str('{:05d}'.format(int(dt.microsecond / 10))) if token == 'SSSS': - return str('{0:04d}'.format(int(dt.microsecond / 100))) + return str('{:04d}'.format(int(dt.microsecond / 100))) if token == 'SSS': - return str('{0:03d}'.format(int(dt.microsecond / 1000))) + return str('{:03d}'.format(int(dt.microsecond / 1000))) if token == 'SS': - return str('{0:02d}'.format(int(dt.microsecond / 10000))) + return str('{:02d}'.format(int(dt.microsecond / 10000))) if token == 'S': return str(int(dt.microsecond / 100000)) @@ -101,7 +101,7 @@ def _format_token(self, dt, token): total_minutes = abs(total_minutes) hour, minute = divmod(total_minutes, 60) - return '{0}{1:02d}{2}{3:02d}'.format(sign, hour, separator, minute) + return '{}{:02d}{}{:02d}'.format(sign, hour, separator, minute) if token in ('a', 'A'): return self.locale.meridian(dt.hour, token) diff --git a/arrow/locales.py b/arrow/locales.py index 17113b8d2..587a53681 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -18,7 +18,7 @@ def get_locale(name): locale_cls = _locales.get(name.lower()) if locale_cls is None: - raise ValueError('Unsupported locale \'{0}\''.format(name)) + raise ValueError('Unsupported locale \'{}\''.format(name)) return locale_cls() @@ -135,14 +135,14 @@ def year_full(self, year): :param name: the ``int`` year (4-digit) ''' - return '{0:04d}'.format(year) + return '{:04d}'.format(year) def year_abbreviation(self, year): ''' Returns the year for specific locale if available :param name: the ``int`` year (4-digit) ''' - return '{0:04d}'.format(year)[2:] + return '{:04d}'.format(year)[2:] def meridian(self, hour, token): ''' Returns the meridian indicator for a specified hour and format token. @@ -164,7 +164,7 @@ def ordinal_number(self, n): return self._ordinal_number(n) def _ordinal_number(self, n): - return '{0}'.format(n) + return '{}'.format(n) def _name_to_ordinal(self, lst): return dict(map(lambda i: (i[1].lower(), i[0] + 1), enumerate(lst[1:]))) @@ -227,12 +227,12 @@ def _ordinal_number(self, n): if n % 100 not in (11, 12, 13): remainder = abs(n) % 10 if remainder == 1: - return '{0}st'.format(n) + return '{}st'.format(n) elif remainder == 2: - return '{0}nd'.format(n) + return '{}nd'.format(n) elif remainder == 3: - return '{0}rd'.format(n) - return '{0}th'.format(n) + return '{}rd'.format(n) + return '{}th'.format(n) class ItalianLocale(Locale): @@ -266,7 +266,7 @@ class ItalianLocale(Locale): ordinal_day_re = r'((?P[1-3]?[0-9](?=[ºª]))[ºª])' def _ordinal_number(self, n): - return '{0}º'.format(n) + return '{}º'.format(n) class SpanishLocale(Locale): @@ -300,7 +300,7 @@ class SpanishLocale(Locale): ordinal_day_re = r'((?P[1-3]?[0-9](?=[ºª]))[ºª])' def _ordinal_number(self, n): - return '{0}º'.format(n) + return '{}º'.format(n) class FrenchLocale(Locale): @@ -335,8 +335,8 @@ class FrenchLocale(Locale): def _ordinal_number(self, n): if abs(n) == 1: - return '{0}er'.format(n) - return '{0}e'.format(n) + return '{}er'.format(n) + return '{}e'.format(n) class GreekLocale(Locale): @@ -485,7 +485,7 @@ def _format_relative(self, humanized, timeframe, delta): return direction.format(humanized[which]) def _ordinal_number(self, n): - return '{0}.'.format(n) + return '{}.'.format(n) class ChineseCNLocale(Locale): @@ -832,7 +832,7 @@ class _DeutschLocaleCommonMixin(object): ] def _ordinal_number(self, n): - return '{0}.'.format(n) + return '{}.'.format(n) class GermanLocale(_DeutschLocaleCommonMixin, Locale): @@ -984,7 +984,7 @@ class TagalogLocale(Locale): day_abbreviations = ['', 'Lun', 'Mar', 'Miy', 'Huw', 'Biy', 'Sab', 'Lin'] def _ordinal_number(self, n): - return 'ika-{0}'.format(n) + return 'ika-{}'.format(n) class VietnameseLocale(Locale): @@ -1590,7 +1590,7 @@ class HebrewLocale(Locale): def _format_timeframe(self, timeframe, delta): '''Hebrew couple of aware''' - couple = '2-{0}'.format(timeframe) + couple = '2-{}'.format(timeframe) if abs(delta) == 2 and couple in self.timeframes: return self.timeframes[couple].format(abs(delta)) else: @@ -1797,7 +1797,7 @@ class EsperantoLocale(Locale): ordinal_day_re = r'((?P[1-3]?[0-9](?=a))a)' def _ordinal_number(self, n): - return '{0}a'.format(n) + return '{}a'.format(n) class ThaiLocale(Locale): @@ -1845,12 +1845,12 @@ class ThaiLocale(Locale): def year_full(self, year): '''Thai always use Buddhist Era (BE) which is CE + 543''' year += self.BE_OFFSET - return '{0:04d}'.format(year) + return '{:04d}'.format(year) def year_abbreviation(self, year): '''Thai always use Buddhist Era (BE) which is CE + 543''' year += self.BE_OFFSET - return '{0:04d}'.format(year)[2:] + return '{:04d}'.format(year)[2:] def _format_relative(self, humanized, timeframe, delta): '''Thai normally doesn't have any space between words''' @@ -1902,15 +1902,15 @@ class BengaliLocale(Locale): def _ordinal_number(self, n): if n > 10 or n == 0: - return '{0}তম'.format(n) + return '{}তম'.format(n) if n in [1, 5, 7, 8, 9, 10]: - return '{0}ম'.format(n) + return '{}ম'.format(n) if n in [2, 3]: - return '{0}য়'.format(n) + return '{}য়'.format(n) if n == 4: - return '{0}র্থ'.format(n) + return '{}র্থ'.format(n) if n == 6: - return '{0}ষ্ঠ'.format(n) + return '{}ষ্ঠ'.format(n) class RomanshLocale(Locale): diff --git a/arrow/parser.py b/arrow/parser.py index df8cc1a47..e3a27ea80 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -143,8 +143,8 @@ def _generate_pattern_re(self, fmt): try: input_re = self._input_re_map[token] except KeyError: - raise ParserError('Unrecognized token \'{0}\''.format(token)) - input_pattern = '(?P<{0}>{1})'.format(token, input_re.pattern) + raise ParserError('Unrecognized token \'{}\''.format(token)) + input_pattern = '(?P<{}>{})'.format(token, input_re.pattern) tokens.append(token) # a pattern doesn't have the same length as the token # it replaces! We keep the difference in the offset variable. @@ -174,7 +174,7 @@ def parse(self, string, fmt): match = fmt_pattern_re.search(string) if match is None: - raise ParserError('Failed to match \'{0}\' when parsing \'{1}\'' + raise ParserError('Failed to match \'{}\' when parsing \'{}\'' .format(fmt_pattern_re.pattern, string)) parts = {} for token in fmt_tokens: @@ -283,7 +283,7 @@ def _parse_multiformat(self, string, formats): pass if _datetime is None: - raise ParserError('Could not match input to any of {0} on \'{1}\''.format(formats, string)) + raise ParserError('Could not match input to any of {} on \'{}\''.format(formats, string)) return _datetime @@ -293,7 +293,7 @@ def _map_lookup(input_map, key): try: return input_map[key] except KeyError: - raise ParserError('Could not match "{0}" to {1}'.format(key, input_map)) + raise ParserError('Could not match "{}" to {}'.format(key, input_map)) @staticmethod def _try_timestamp(string): @@ -305,7 +305,7 @@ def _try_timestamp(string): @staticmethod def _choice_re(choices, flags=0): - return re.compile('({0})'.format('|'.join(choices)), flags=flags) + return re.compile('({})'.format('|'.join(choices)), flags=flags) class TzinfoParser(object): @@ -342,6 +342,6 @@ def parse(cls, string): tzinfo = tz.gettz(string) if tzinfo is None: - raise ParserError('Could not parse timezone expression "{0}"'.format(string)) + raise ParserError('Could not parse timezone expression "{}"'.format(string)) return tzinfo diff --git a/arrow/util.py b/arrow/util.py index 088e5dfd6..50b99db39 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -5,23 +5,9 @@ import sys import warnings -# python 2.6 / 2.7 definitions for total_seconds function. - -def _total_seconds_27(td): # pragma: no cover +def total_seconds(td): # pragma: no cover return td.total_seconds() -def _total_seconds_26(td): - return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 1e6) / 1e6 - - -# get version info and assign correct total_seconds function. - -version = '{0}.{1}.{2}'.format(*sys.version_info[:3]) - -if version < '2.7': # pragma: no cover - total_seconds = _total_seconds_26 -else: # pragma: no cover - total_seconds = _total_seconds_27 def is_timestamp(value): if type(value) == bool: @@ -32,15 +18,16 @@ def is_timestamp(value): except: return False -# python 2.7 / 3.0+ definitions for isstr function. -try: # pragma: no cover +# Python 2.7 / 3.0+ definitions for isstr function. + +try: # pragma: no cover basestring def isstr(s): return isinstance(s, basestring) -except NameError: #pragma: no cover +except NameError: # pragma: no cover def isstr(s): return isinstance(s, str) @@ -84,7 +71,7 @@ def wrapper(self, *args, **kwargs): __setitem__ = _wrap_method('__setitem__') __delitem__ = _wrap_method('__delitem__') append = _wrap_method('append') - if version >= '3.0': # pragma: no cover + if sys.version_info.major >= 3: # pragma: no cover clear = _wrap_method('clear') copy = _wrap_method('copy') extend = _wrap_method('extend') diff --git a/docs/index.rst b/docs/index.rst index 3a2c6f8c1..6aec3da27 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,7 +26,7 @@ Features -------- - Fully implemented, drop-in replacement for datetime -- Supports Python 2.6, 2.7, 3.3, 3.4 and 3.5 +- Supports Python 2.7, 3.4, 3.5 and 3.6 - Timezone-aware & UTC by default - Provides super-simple creation options for many common input scenarios - Updated .replace method with support for relative offsets, including weeks diff --git a/requirements26.txt b/requirements26.txt deleted file mode 100644 index 92896c107..000000000 --- a/requirements26.txt +++ /dev/null @@ -1 +0,0 @@ -chai==0.3.1 diff --git a/setup.py b/setup.py index 933e122ad..043357e08 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,6 @@ from distutils.core import setup - def fpath(name): return os.path.join(os.path.dirname(__file__), name) @@ -19,7 +18,7 @@ def read(fname): def grep(attrname): - pattern = r"{0}\W*=\W*'([^']+)'".format(attrname) + pattern = r"{}\W*=\W*'([^']+)'".format(attrname) strval, = re.findall(pattern, file_text) return strval @@ -37,6 +36,7 @@ def grep(attrname): license='Apache 2.0', packages=['arrow'], zip_safe=False, + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*', install_requires=[ 'python-dateutil', ], @@ -48,12 +48,10 @@ def grep(attrname): 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Software Development :: Libraries :: Python Modules' ] ) - diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 9c34ea0f6..afa3ca3ea 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -106,7 +106,7 @@ def test_repr(self): result = self.arrow.__repr__() - assertEqual(result, ''.format(self.arrow._datetime.isoformat())) + assertEqual(result, ''.format(self.arrow._datetime.isoformat())) def test_str(self): @@ -122,7 +122,7 @@ def test_hash(self): def test_format(self): - result = '{0:YYYY-MM-DD}'.format(self.arrow) + result = '{:YYYY-MM-DD}'.format(self.arrow) assertEqual(result, '2013-02-03') @@ -134,7 +134,7 @@ def test_bare_format(self): def test_format_no_format_string(self): - result = '{0}'.format(self.arrow) + result = '{}'.format(self.arrow) assertEqual(result, str(self.arrow)) @@ -1377,7 +1377,7 @@ def test_get_datetime(self): with assertRaises(ValueError) as raise_ctx: get_datetime('abc') - assertFalse('{0}' in str(raise_ctx.exception)) + assertFalse('{}' in str(raise_ctx.exception)) def test_get_tzinfo(self): @@ -1385,7 +1385,7 @@ def test_get_tzinfo(self): with assertRaises(ValueError) as raise_ctx: get_tzinfo('abc') - assertFalse('{0}' in str(raise_ctx.exception)) + assertFalse('{}' in str(raise_ctx.exception)) def test_get_timestamp_from_input(self): @@ -1447,7 +1447,7 @@ def newshim(): shim[0:0] = [] # doesn't warn on py2 del shim[0:0] # doesn't warn on py2 newshim().append(6) - if util.version >= '3.0': # pragma: no cover + if sys.version_info.major >= 3: # pragma: no cover newshim().clear() shim.copy() shim.extend([]) @@ -1459,7 +1459,7 @@ def newshim(): newshim().reverse() newshim().sort() - if util.version >= '3.0': # pragma: no cover + if sys.version_info.major >= 3: # pragma: no cover assertEqual(19, len(w)) else: # pragma: no cover assertEqual(15, len(w)) diff --git a/tests/util_tests.py b/tests/util_tests.py deleted file mode 100644 index c3d059fdd..000000000 --- a/tests/util_tests.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- - -from chai import Chai -from datetime import timedelta -import sys - -from arrow import util - - -class UtilTests(Chai): - - def setUp(self): - super(UtilTests, self).setUp() - - def test_total_seconds_26(self): - - td = timedelta(seconds=30) - - assertEqual(util._total_seconds_26(td), 30) - - if util.version >= '2.7': # pragma: no cover - - def test_total_seconds_27(self): - - td = timedelta(seconds=30) - - assertEqual(util._total_seconds_27(td), 30) - diff --git a/tox.ini b/tox.ini index cd97a9129..64c0b2d25 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py33,py34,py35 +envlist = py27,py34,py35,py36 skip_missing_interpreters = True [common] From 1f6d86f23923ed85e5a5453e71389adf1eed050d Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 21 Dec 2018 13:12:59 +0200 Subject: [PATCH 144/649] Add tests_require=['chai'] (#473) * Add test_requirements=['chai'] * Add tests_require=['chai'], --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 043357e08..cb69ea342 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ def grep(attrname): ":python_version=='2.7'": ['backports.functools_lru_cache>=1.2.1'], }, test_suite="tests", + tests_require=['chai'], classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', From 9e2c50e38d63dcca7f2f2ff1f8b0400d36fb0e80 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Mon, 7 Jan 2019 17:13:24 -0800 Subject: [PATCH 145/649] bump verison to 0.13.0 --- arrow/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/__init__.py b/arrow/__init__.py index 86ccb69f0..90d4fd23b 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -4,5 +4,5 @@ from .factory import ArrowFactory from .api import get, now, utcnow -__version__ = '0.12.1' +__version__ = '0.13.0' VERSION = __version__ From 32f9a8c121f58bb96b14f6909082b888765d2f43 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Wed, 9 Jan 2019 20:33:49 +0000 Subject: [PATCH 146/649] Update HISTORY.md with 0.13.0 --- HISTORY.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 39562bcff..931578e64 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,22 @@ ## History +### 0.13.0 + +- [NEW] Added support for Python 3.6. +- [CHANGE] Drop support for Python 2.6/3.3. +- [CHANGE] Return generator instead of list for Arrow.range(), Arrow.span_range() and Arrow.interval(). +- [FIX] Make arrow.get() work with str & tzinfo combo. +- [FIX] Make sure special RegEx characters are escaped in format string. +- [NEW] Added support for ZZZ when formatting. +- [FIX] Stop using datetime.utcnow() in internals, use datetime.now(UTC) instead. +- [FIX] Return NotImplemented instead of TypeError in arrow math internals. +- [NEW] Added Estonian Locale. +- [FIX] Small fixes to Greek locale. +- [FIX] TagalogLocale improvements. +- [FIX] Added test requirements to setup. +- [FIX] Improve docs for get, now and utcnow methods. +- [FIX] Correct typo in depreciation warning. + ### 0.12.1 - [FIX] Allow universal wheels to be generated and reliably installed. From f6759989a51f2236165f0a9a42f96151ad9629db Mon Sep 17 00:00:00 2001 From: Kalle Bronsen Date: Fri, 6 Jul 2018 14:25:43 +0200 Subject: [PATCH 147/649] Remove double assignment of `later`. --- arrow/arrow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index dedf90711..c9277bc0a 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -722,7 +722,7 @@ def humanize(self, other=None, locale='en_us', only_distance=False, granularity= >>> earlier.humanize() '2 hours ago' - >>> later = later = earlier.shift(hours=4) + >>> later = earlier.shift(hours=4) >>> later.humanize(earlier) 'in 4 hours' From ad33776f81e0edb4aa5f1792e91e233774b2303d Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Tue, 29 Jan 2019 16:29:16 +0000 Subject: [PATCH 148/649] Update docs to 0.13.0 - Add usage section to more methods and properties. - Make locales appear in readthedocs. - Small updates and additions. --- arrow/arrow.py | 206 ++++++++++++++++++++++++++++++++++++++++++----- arrow/locales.py | 5 +- docs/conf.py | 6 +- docs/index.rst | 11 ++- 4 files changed, 201 insertions(+), 27 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index c9277bc0a..a944abc7a 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -77,6 +77,11 @@ def now(cls, tzinfo=None): :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. + Usage:: + + >>> arrow.now('Asia/Baku') + + ''' tzinfo = tzinfo if tzinfo is not None else dateutil_tz.tzlocal() @@ -90,6 +95,11 @@ def utcnow(cls): ''' Constructs an :class:`Arrow ` object, representing "now" in UTC time. + Usage:: + + >>> arrow.utcnow() + + ''' dt = datetime.now(dateutil_tz.tzutc()) @@ -183,6 +193,11 @@ def strptime(cls, date_str, fmt, tzinfo=None): :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to the parsed timezone if ``fmt`` contains a timezone directive, otherwise UTC. + Usage:: + + >>> arrow.Arrow.strptime('20-01-2019 15:49:10', '%d-%m-%Y %H:%M:%S') + + ''' dt = datetime.strptime(date_str, fmt) @@ -404,7 +419,15 @@ def __getattr__(self, name): @property def tzinfo(self): - ''' Gets the ``tzinfo`` of the :class:`Arrow ` object. ''' + ''' Gets the ``tzinfo`` of the :class:`Arrow ` object. + + Usage:: + + >>> arw=arrow.utcnow() + >>> arw.tzinfo + tzutc() + + ''' return self._datetime.tzinfo @@ -416,28 +439,60 @@ def tzinfo(self, tzinfo): @property def datetime(self): - ''' Returns a datetime representation of the :class:`Arrow ` object. ''' + ''' Returns a datetime representation of the :class:`Arrow ` object. + + Usage:: + + >>> arw=arrow.utcnow() + >>> arw.datetime + datetime.datetime(2019, 1, 24, 16, 35, 27, 276649, tzinfo=tzutc()) + + ''' return self._datetime @property def naive(self): ''' Returns a naive datetime representation of the :class:`Arrow ` - object. ''' + object. + + Usage:: + + >>> nairobi = arrow.now('Africa/Nairobi') + >>> nairobi + + >>> nairobi.naive + datetime.datetime(2019, 1, 23, 19, 27, 12, 297999) + + ''' return self._datetime.replace(tzinfo=None) @property def timestamp(self): ''' Returns a timestamp representation of the :class:`Arrow ` object, in - UTC time. ''' + UTC time. + + Usage:: + + >>> arrow.utcnow().timestamp + 1548260567 + + ''' return calendar.timegm(self._datetime.utctimetuple()) @property def float_timestamp(self): ''' Returns a floating-point representation of the :class:`Arrow ` - object, in UTC time. ''' + object, in UTC time. + + Usage:: + + >>> arrow.utcnow().float_timestamp + 1548260516.830896 + + ''' return self.timestamp + float(self.microsecond) / 1000000 @@ -716,6 +771,7 @@ def humanize(self, other=None, locale='en_us', only_distance=False, granularity= :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'. :param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part. :param granularity: (optional) defines the precision of the output. Set it to strings 'second', 'minute', 'hour', 'day', 'month' or 'year'. + Usage:: >>> earlier = arrow.utcnow().shift(hours=-2) @@ -903,18 +959,39 @@ def __cmp__(self, other): # datetime methods def date(self): - ''' Returns a ``date`` object with the same year, month and day. ''' + ''' Returns a ``date`` object with the same year, month and day. + + Usage:: + + >>> arrow.utcnow().date() + datetime.date(2019, 1, 23) + + ''' return self._datetime.date() def time(self): - ''' Returns a ``time`` object with the same hour, minute, second, microsecond. ''' + ''' Returns a ``time`` object with the same hour, minute, second, microsecond. + + Usage:: + + >>> arrow.utcnow().time() + datetime.time(12, 15, 34, 68352) + + ''' return self._datetime.time() def timetz(self): ''' Returns a ``time`` object with the same hour, minute, second, microsecond and - tzinfo. ''' + tzinfo. + + Usage:: + + >>> arrow.utcnow().timetz() + datetime.time(12, 5, 18, 298893, tzinfo=tzutc()) + + ''' return self._datetime.timetz() @@ -923,72 +1000,161 @@ def astimezone(self, tz): :param tz: a ``tzinfo`` object. + Usage:: + + >>> pacific=arrow.now('US/Pacific') + >>> nyc=arrow.now('America/New_York').tzinfo + >>> pacific.astimezone(nyc) + datetime.datetime(2019, 1, 20, 10, 24, 22, 328172, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York')) + ''' return self._datetime.astimezone(tz) def utcoffset(self): ''' Returns a ``timedelta`` object representing the whole number of minutes difference from - UTC time. ''' + UTC time. + + Usage:: + + >>> arrow.now('US/Pacific').utcoffset() + datetime.timedelta(-1, 57600) + + ''' return self._datetime.utcoffset() def dst(self): - ''' Returns the daylight savings time adjustment. ''' + ''' Returns the daylight savings time adjustment. + + Usage:: + + >>> arrow.utcnow().dst() + datetime.timedelta(0) + + ''' return self._datetime.dst() def timetuple(self): - ''' Returns a ``time.struct_time``, in the current timezone. ''' + ''' Returns a ``time.struct_time``, in the current timezone. + + Usage:: + + >>> arrow.utcnow().timetuple() + time.struct_time(tm_year=2019, tm_mon=1, tm_mday=20, tm_hour=15, tm_min=17, tm_sec=8, tm_wday=6, tm_yday=20, tm_isdst=0) + + ''' return self._datetime.timetuple() def utctimetuple(self): - ''' Returns a ``time.struct_time``, in UTC time. ''' + ''' Returns a ``time.struct_time``, in UTC time. + + Usage:: + + >>> arrow.utcnow().utctimetuple() + time.struct_time(tm_year=2019, tm_mon=1, tm_mday=19, tm_hour=21, tm_min=41, tm_sec=7, tm_wday=5, tm_yday=19, tm_isdst=0) + + ''' return self._datetime.utctimetuple() def toordinal(self): - ''' Returns the proleptic Gregorian ordinal of the date. ''' + ''' Returns the proleptic Gregorian ordinal of the date. + + Usage:: + + >>> arrow.utcnow().toordinal() + 737078 + + ''' return self._datetime.toordinal() def weekday(self): - ''' Returns the day of the week as an integer (0-6). ''' + ''' Returns the day of the week as an integer (0-6). + + Usage:: + + >>> arrow.utcnow().weekday() + 5 + + ''' return self._datetime.weekday() def isoweekday(self): - ''' Returns the ISO day of the week as an integer (1-7). ''' + ''' Returns the ISO day of the week as an integer (1-7). + + Usage:: + + >>> arrow.utcnow().isoweekday() + 6 + + ''' return self._datetime.isoweekday() def isocalendar(self): - ''' Returns a 3-tuple, (ISO year, ISO week number, ISO weekday). ''' + ''' Returns a 3-tuple, (ISO year, ISO week number, ISO weekday). + + Usage:: + + >>> arrow.utcnow().isocalendar() + (2019, 3, 6) + + ''' return self._datetime.isocalendar() def isoformat(self, sep='T'): - '''Returns an ISO 8601 formatted representation of the date and time. ''' + '''Returns an ISO 8601 formatted representation of the date and time. + + Usage:: + + >>> arrow.utcnow().isoformat() + '2019-01-19T18:30:52.442118+00:00' + + ''' return self._datetime.isoformat(sep) def ctime(self): - ''' Returns a ctime formatted representation of the date and time. ''' + ''' Returns a ctime formatted representation of the date and time. + + Usage:: + + >>> arrow.utcnow().ctime() + 'Sat Jan 19 18:26:50 2019' + + ''' return self._datetime.ctime() def strftime(self, format): - ''' Formats in the style of ``datetime.strptime``. + ''' Formats in the style of ``datetime.strftime``. :param format: the format string. + Usage:: + + >>> arrow.utcnow().strftime('%d-%m-%Y %H:%M:%S') + '23-01-2019 12:28:17' + ''' return self._datetime.strftime(format) def for_json(self): - '''Serializes for the ``for_json`` protocol of simplejson.''' + '''Serializes for the ``for_json`` protocol of simplejson. + + Usage:: + + >>> arrow.utcnow().for_json() + '2019-01-19T18:25:36.760079+00:00' + + ''' return self.isoformat() diff --git a/arrow/locales.py b/arrow/locales.py index 587a53681..09e7b21af 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1132,7 +1132,7 @@ class ArabicLocale(Locale): day_names = ['', 'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'] day_abbreviations = ['', 'إثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت', 'أحد'] - + def _format_timeframe(self, timeframe, delta): form = self.timeframes[timeframe] delta = abs(delta) @@ -2194,6 +2194,5 @@ def _format_timeframe(self, timeframe, delta): else: form = form['past'] return form.format(abs(delta)) - - + _locales = _map_locales() diff --git a/docs/conf.py b/docs/conf.py index 9313847af..3d9b00438 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,16 +41,16 @@ # General information about the project. project = u'Arrow' -copyright = u'2013, Chris Smith' +copyright = u'2019, Chris Smith' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.11.0' +version = '0.13.0' # The full version, including alpha/beta/rc tags. -release = '0.11.0' +release = '0.13.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/index.rst b/docs/index.rst index 6aec3da27..6cdc4726e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -34,7 +34,7 @@ Features - Partial ISO-8601 support - Timezone conversion - Timestamp available as a property -- Generates time spans, ranges, floors and ceilings in time frames from year to microsecond +- Generates time spans, ranges, floors and ceilings in time frames from year to microsecond precision - Humanizes and supports a growing list of contributed locales - Extensible for your own Arrow-derived types @@ -454,6 +454,14 @@ Use the following tokens in parsing and formatting. Note that they're not the s .. [#t3] the result is truncated to microseconds, with `half-to-even rounding `_. .. [#t4] timezone names from `tz database `_ provided via dateutil package +Any token can be escaped when parsing by enclosing it within square brackets: + +.. code-block:: python + + >>> arrow.get("2018-03-09 8 h 40", "YYYY-MM-DD h [h] m") + + + --------- API Guide --------- @@ -481,3 +489,4 @@ arrow.locale .. automodule:: arrow.locales :members: + :undoc-members: From c94bf9fbd41180ca9a0b3bf24089753067d34c59 Mon Sep 17 00:00:00 2001 From: Kris Fremen Date: Sat, 9 Feb 2019 16:15:48 +0000 Subject: [PATCH 149/649] Python 3.7 support. (#544) * Python 3.7 support. * Python 3.7 - Adding travis builds and tox. * Python 3.7 - Travis supports only 3.7-dev for now. * Update Makefile * Update .travis.yml --- .travis.yml | 3 +++ Makefile | 4 ++++ arrow/arrow.py | 14 +++++++------- arrow/util.py | 8 -------- tox.ini | 2 +- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index 713403dc8..5b5996985 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,18 @@ sudo: false language: python +dist: xenial python: - 2.7 - 3.4 - 3.5 - 3.6 + - 3.7 install: - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then make build27; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then make build34; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then make build35; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then make build36; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.7' ]]; then make build37; fi - pip install codecov script: make test after_success: codecov diff --git a/Makefile b/Makefile index adabbd6ef..74f57bacc 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,10 @@ build36: virtualenv local --python=python3.6 local/bin/pip install -r requirements.txt +build37: + virtualenv local --python=python3.7 + local/bin/pip install -r requirements.txt + test: rm -f .coverage . local/bin/activate && nosetests diff --git a/arrow/arrow.py b/arrow/arrow.py index a944abc7a..8db28bed1 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -210,7 +210,6 @@ def strptime(cls, date_str, fmt, tzinfo=None): # factories: ranges and spans @classmethod - @util.list_to_iter_deprecation def range(cls, frame, start, end=None, tz=None, limit=None): ''' Returns an iterator of :class:`Arrow ` objects, representing points in time between two inputs. @@ -282,7 +281,6 @@ def range(cls, frame, start, end=None, tz=None, limit=None): @classmethod - @util.list_to_iter_deprecation def span_range(cls, frame, start, end, tz=None, limit=None): ''' Returns an iterator of tuples, each :class:`Arrow ` objects, representing a series of timespans between two inputs. @@ -334,7 +332,6 @@ def span_range(cls, frame, start, end, tz=None, limit=None): return (r.span(frame) for r in _range) @classmethod - @util.list_to_iter_deprecation def interval(cls, frame, start, end, interval=1, tz=None): ''' Returns an iterator of tuples, each :class:`Arrow ` objects, representing a series of intervals between two inputs. @@ -375,10 +372,13 @@ def interval(cls, frame, start, end, interval=1, tz=None): spanRange = iter(cls.span_range(frame, start, end, tz)) while True: - intvlStart, intvlEnd = next(spanRange) # StopIteration when exhausted - for _ in range(interval-1): - _, intvlEnd = next(spanRange) # StopIteration when exhausted - yield intvlStart, intvlEnd + try: + intvlStart, intvlEnd = next(spanRange) + for _ in range(interval-1): + _, intvlEnd = next(spanRange) + yield intvlStart, intvlEnd + except StopIteration: + return # representations diff --git a/arrow/util.py b/arrow/util.py index 50b99db39..5358ef13a 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -86,12 +86,4 @@ def wrapper(self, *args, **kwargs): del _wrap_method -def list_to_iter_deprecation(f): - warn_text = '{0}() will return an iterator in a future release, convert to list({0}())'.format(f.__name__) - @functools.wraps(f) - def wrapper(*args, **kwargs): - return list_to_iter_shim(f(*args, **kwargs), warn_text=warn_text) - return wrapper - - __all__ = ['total_seconds', 'is_timestamp', 'isstr', 'list_to_iter_shim', 'list_to_iter_deprecation'] diff --git a/tox.ini b/tox.ini index 64c0b2d25..50e6fc93e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36 +envlist = py27,py34,py35,py36,py37 skip_missing_interpreters = True [common] From 2e551688111e0d33c9ae723f4ab7a2f9cef15f8d Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Sat, 9 Feb 2019 16:57:57 +0000 Subject: [PATCH 150/649] Mention Python 3.7 in docs (#564) --- README.rst | 12 ++++++------ docs/index.rst | 2 +- setup.py | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 125a37165..ab87cda8a 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ Arrow - Better dates & times for Python .. image:: https://img.shields.io/pypi/v/arrow.svg :target: https://pypi.python.org/pypi/arrow :alt: downloads - + Documentation: `arrow.readthedocs.org `_ --------------------------------------------------------------------------------- @@ -30,15 +30,15 @@ Python's standard library and some other low-level modules have near-complete da - Too many modules: datetime, time, calendar, dateutil, pytz and more - Too many types: date, time, datetime, tzinfo, timedelta, relativedelta, etc. -- Time zones and timestamp conversions are verbose and unpleasant +- Time zones and timestamp conversions are verbose and unpleasant - Time zone naievety is the norm - Gaps in functionality: ISO-8601 parsing, timespans, humanization -Features +Features -------- -- Fully implemented, drop-in replacement for datetime -- Supports Python 2.7, 3.4, 3.5 and 3.6 +- Fully implemented, drop-in replacement for datetime +- Supports Python 2.7, 3.4, 3.5, 3.6 and 3.7 - Time zone-aware & UTC by default - Provides super-simple creation options for many common input scenarios - Updated .replace method with support for relative offsets, including weeks @@ -93,7 +93,7 @@ And then: >>> local.humanize(locale='ko_kr') '1시간 전' - + Further documentation can be found at `arrow.readthedocs.org `_ Contributing diff --git a/docs/index.rst b/docs/index.rst index 6cdc4726e..8e2f1d8fe 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,7 +26,7 @@ Features -------- - Fully implemented, drop-in replacement for datetime -- Supports Python 2.7, 3.4, 3.5 and 3.6 +- Supports Python 2.7, 3.4, 3.5, 3.6 and 3.7 - Timezone-aware & UTC by default - Provides super-simple creation options for many common input scenarios - Updated .replace method with support for relative offsets, including weeks diff --git a/setup.py b/setup.py index cb69ea342..4c1d6d03e 100644 --- a/setup.py +++ b/setup.py @@ -53,6 +53,7 @@ def grep(attrname): 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Software Development :: Libraries :: Python Modules' ] ) From 5b175882be0cd8e9faba3ed0f3c27a5bb5ba756f Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 12 Feb 2019 20:49:07 -0800 Subject: [PATCH 151/649] version bump --- arrow/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/__init__.py b/arrow/__init__.py index 90d4fd23b..23029c79b 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -4,5 +4,5 @@ from .factory import ArrowFactory from .api import get, now, utcnow -__version__ = '0.13.0' +__version__ = '0.13.1' VERSION = __version__ From d8c7a78038a9c0fde9881c986b32d473ad260f71 Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Wed, 27 Feb 2019 20:35:58 +0000 Subject: [PATCH 152/649] Update HISTORY.md with 0.13.1 (#572) * Update HISTORY.md with 0.13.1 * Mention doc fixes --- HISTORY.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 931578e64..250e4d904 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ ## History +### 0.13.1 + +- [NEW] Add support for Python 3.7. +- [CHANGE] Remove deprecation decorators for Arrow.range(), Arrow.span_range() and Arrow.interval(), all now return generators, wrap with list() to get old behavior. +- [FIX] Documentation and docstring updates. + ### 0.13.0 - [NEW] Added support for Python 3.6. From 43ffd2ac65984ca90d6c44b6e6dc9769f5288117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20D=C3=B6rfler?= Date: Mon, 4 Mar 2019 11:47:55 +0100 Subject: [PATCH 153/649] Add additional timeframes translations for german language (#571) * Add additional timeframes translations for german localisations "only distance" case * Add tests for describe method --- arrow/locales.py | 22 ++++++++++++++++++++++ tests/locales_tests.py | 12 ++++++++++++ 2 files changed, 34 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 09e7b21af..2efc859b4 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -812,6 +812,13 @@ class _DeutschLocaleCommonMixin(object): 'years': '{0} Jahren', } + timeframes_only_distance = timeframes.copy() + timeframes_only_distance['minute'] = 'eine Minute' + timeframes_only_distance['hour'] = 'eine Stunde' + timeframes_only_distance['day'] = 'ein Tag' + timeframes_only_distance['month'] = 'ein Monat' + timeframes_only_distance['year'] = 'ein Jahr' + month_names = [ '', 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember' @@ -834,6 +841,21 @@ class _DeutschLocaleCommonMixin(object): def _ordinal_number(self, n): return '{}.'.format(n) + def describe(self, timeframe, delta=0, only_distance=False): + ''' Describes a delta within a timeframe in plain language. + :param timeframe: a string representing a timeframe. + :param delta: a quantity representing a delta in a timeframe. + :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords + ''' + + humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) + + if not only_distance: + humanized = self._format_timeframe(timeframe, delta) + humanized = self._format_relative(humanized, timeframe, delta) + + return humanized + class GermanLocale(_DeutschLocaleCommonMixin, Locale): diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 9afe6bcfe..82b31ef23 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -417,6 +417,18 @@ def setUp(self): def test_ordinal_number(self): assertEqual(self.locale.ordinal_number(1), '1.') + + def test_define(self): + assertEqual(self.locale.describe("minute", only_distance=True), 'eine Minute') + assertEqual(self.locale.describe("minute", only_distance=False), 'in einer Minute') + assertEqual(self.locale.describe("hour", only_distance=True), 'eine Stunde') + assertEqual(self.locale.describe("hour", only_distance=False), 'in einer Stunde') + assertEqual(self.locale.describe("day", only_distance=True), 'ein Tag') + assertEqual(self.locale.describe("day", only_distance=False), 'in einem Tag') + assertEqual(self.locale.describe("month", only_distance=True), 'ein Monat') + assertEqual(self.locale.describe("month", only_distance=False), 'in einem Monat') + assertEqual(self.locale.describe("year", only_distance=True), 'ein Jahr') + assertEqual(self.locale.describe("year", only_distance=False), 'in einem Jahr') class HungarianLocaleTests(Chai): From be0f8ee4e4cd9967da58ac1aa123ff4d9e498cba Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 25 Apr 2019 12:40:35 -0400 Subject: [PATCH 154/649] Add is_between functionality (#574) * Began work on isbetween feature * Finished implementation of isbetween * Improved function documentation and renamed function to is_between for improved readability. --- arrow/arrow.py | 59 ++++++++++++++++++++++++++++++++++ tests/arrow_tests.py | 76 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/arrow/arrow.py b/arrow/arrow.py index 8db28bed1..08639a34e 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -871,6 +871,65 @@ def humanize(self, other=None, locale='en_us', only_distance=False, granularity= if(trunc(abs(delta)) != 1): granularity += 's' return locale.describe(granularity, delta, only_distance=only_distance) + + # query functions + + def is_between(self, start, end, bounds='()'): + ''' Returns a boolean denoting whether the specified date and time is between + the start and end dates and times. + + :param start: an :class:`Arrow `. + :param end: an :class:`Arrow `. + :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies + whether to include or exclude the start and end values in the range. '(' excludes + the start, '[' includes the start, ')' excludes the end, and ']' includes the end. + If the bounds are not specified, the default bound '()' is used. + + Usage:: + + >>> start = arrow.get(datetime(2013, 5, 5, 12, 30, 10)) + >>> end = arrow.get(datetime(2013, 5, 5, 12, 30, 36)) + >>> arrow.get(datetime(2013, 5, 5, 12, 30, 27)).is_between(start, end) + True + + >>> start = arrow.get(datetime(2013, 5, 5)) + >>> end = arrow.get(datetime(2013, 5, 8)) + >>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[]') + True + + >>> start = arrow.get(datetime(2013, 5, 5)) + >>> end = arrow.get(datetime(2013, 5, 8)) + >>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[)') + False + + ''' + + if bounds != '()' and bounds != '(]' and bounds != '[)' and bounds != '[]': + raise AttributeError('Error. Could not understand the specified bounds. Please select between \ + "()", "(]", "[)", or "[]"') + + if not isinstance(start, Arrow): + raise TypeError('Can\'t parse start date argument type of \'{}\''.format(type(start))) + + if not isinstance(end, Arrow): + raise TypeError('Can\'t parse end date argument type of \'{}\''.format(type(end))) + + include_start = (bounds[0] == '[') + include_end = (bounds[1] == ']') + + target_timestamp = self.float_timestamp + start_timestamp = start.float_timestamp + end_timestamp = end.float_timestamp + + if include_start and include_end: + return target_timestamp >= start_timestamp and target_timestamp <= end_timestamp + elif include_start and not include_end: + return target_timestamp >= start_timestamp and target_timestamp < end_timestamp + elif not include_start and include_end: + return target_timestamp > start_timestamp and target_timestamp <= end_timestamp + else: + return target_timestamp > start_timestamp and target_timestamp < end_timestamp + # math def __add__(self, other): diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index afa3ca3ea..fa173c50a 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1360,6 +1360,82 @@ def test_years(self): assertEqual(result, '2 года назад') +class ArrowIsBetweenTests(Chai): + + def test_start_before_end(self): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) + result = target.is_between(start, end) + assertFalse(result) + + def test_exclusive_exclusive_bounds(self): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 27)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 10)) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 36)) + result = target.is_between(start, end, '()') + assertTrue(result) + result = target.is_between(start, end) + assertTrue(result) + + def test_exclusive_exclusive_bounds_same_date(self): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + result = target.is_between(start, end, '()') + assertFalse(result) + + def test_inclusive_exclusive_bounds(self): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 6)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 4)) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 6)) + result = target.is_between(start, end, '[)') + assertFalse(result) + + def test_exclusive_inclusive_bounds(self): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + result = target.is_between(start, end, '(]') + assertTrue(result) + + def test_inclusive_inclusive_bounds_same_date(self): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + result = target.is_between(start, end, '[]') + assertTrue(result) + + def test_type_error_exception(self): + with assertRaises(TypeError): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + start = datetime(2013, 5, 5) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) + result = target.is_between(start, end) + + with assertRaises(TypeError): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) + end = datetime(2013, 5, 8) + result = target.is_between(start, end) + + with assertRaises(TypeError): + result = target.is_between(None, None) + + def test_attribute_error_exception(self): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) + with assertRaises(AttributeError): + result = target.is_between(start, end, "][") + with assertRaises(AttributeError): + result = target.is_between(start, end, "") + with assertRaises(AttributeError): + result = target.is_between(start, end, "]") + with assertRaises(AttributeError): + result = target.is_between(start, end, "[") + with assertRaises(AttributeError): + result = target.is_between(start, end, "hello") class ArrowUtilTests(Chai): From c9cecaf91e7e94fd412019770019d97a03436010 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Fri, 26 Apr 2019 04:31:27 -0400 Subject: [PATCH 155/649] Improved humanize behavior for near-zero durations (#576) * Fixed humanize for near-zero duration referenced in issue #416 * Changed double quotes to single quotes * Fixed build issue with Python 2.7 --- arrow/locales.py | 14 ++++++++++++++ tests/locales_tests.py | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 2efc859b4..db9433a09 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -234,6 +234,19 @@ def _ordinal_number(self, n): return '{}rd'.format(n) return '{}th'.format(n) + def describe(self, timeframe, delta=0, only_distance=False): + ''' Describes a delta within a timeframe in plain language. + + :param timeframe: a string representing a timeframe. + :param delta: a quantity representing a delta in a timeframe. + :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords + ''' + + humanized = super(EnglishLocale, self).describe(timeframe, delta, only_distance) + if only_distance and timeframe == 'now': + humanized = 'instantly' + + return humanized class ItalianLocale(Locale): names = ['it', 'it_it'] @@ -843,6 +856,7 @@ def _ordinal_number(self, n): def describe(self, timeframe, delta=0, only_distance=False): ''' Describes a delta within a timeframe in plain language. + :param timeframe: a string representing a timeframe. :param delta: a quantity representing a delta in a timeframe. :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 82b31ef23..adaab866f 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -96,6 +96,18 @@ def test_meridian_invalid_token(self): assertEqual(self.locale.meridian(7, 'NONSENSE'), None) +class EnglishLocaleTests(Chai): + + def setUp(self): + super(EnglishLocaleTests, self).setUp() + + self.locale = locales.EnglishLocale() + + def test_describe(self): + assertEqual(self.locale.describe('now', only_distance=True), 'instantly') + assertEqual(self.locale.describe('now', only_distance=False), 'just now') + + class ItalianLocalesTests(Chai): def test_ordinal_number(self): From 88de0faf97aebe3f8ec2ecaf30c7d1064d4001f6 Mon Sep 17 00:00:00 2001 From: Troy Stacer Date: Fri, 26 Apr 2019 04:33:50 -0400 Subject: [PATCH 156/649] Fix humanize behavior with future days (#575) * Fixed humanize bug with future dates * Added documentation to test cases --- arrow/arrow.py | 2 +- tests/arrow_tests.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 08639a34e..f4563d9eb 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -802,7 +802,7 @@ def humanize(self, other=None, locale='en_us', only_distance=False, granularity= else: raise TypeError() - delta = int(util.total_seconds(self._datetime - dt)) + delta = int(round(util.total_seconds(self._datetime - dt))) sign = -1 if delta < 0 else 1 diff = abs(delta) delta = diff diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index fa173c50a..851f88939 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1244,6 +1244,16 @@ def test_days(self): assertEqual(self.now.humanize(later, only_distance=True), '2 days') assertEqual(later.humanize(self.now, only_distance=True), '2 days') + # Regression tests for humanize bug referenced in issue 541 + later = self.now.shift(days=3) + assertEqual(later.humanize(), 'in 3 days') + + later = self.now.shift(days=3, seconds=1) + assertEqual(later.humanize(), 'in 3 days') + + later = self.now.shift(days=4) + assertEqual(later.humanize(), 'in 4 days') + def test_month(self): later = self.now.shift(months=1) From aff51a05958c3da880cde760ea589bdf403a1727 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 13 May 2019 06:00:10 -0400 Subject: [PATCH 157/649] Updated README links and added more contributing information (#582) * Updated README to add missing periods and added to contributing section * Fixed RST formatting of links * Another formatting issue * Tweaked some wording * Updated links --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index ab87cda8a..2a43ad5ce 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,7 @@ Arrow - Better dates & times for Python :target: https://pypi.python.org/pypi/arrow :alt: downloads -Documentation: `arrow.readthedocs.org `_ +Documentation: `arrow.readthedocs.io `_ --------------------------------------------------------------------------------- What? @@ -21,7 +21,7 @@ What? Arrow is a Python library that offers a sensible, human-friendly approach to creating, manipulating, formatting and converting dates, times, and timestamps. It implements and updates the datetime type, plugging gaps in functionality, and provides an intelligent module API that supports many common creation scenarios. Simply put, it helps you work with dates and times with fewer imports and a lot less code. -Arrow is heavily inspired by `moment.js `_ and `requests `_ +Arrow is heavily inspired by `moment.js `_ and `requests `_. Why? ---- @@ -94,9 +94,9 @@ And then: >>> local.humanize(locale='ko_kr') '1시간 전' -Further documentation can be found at `arrow.readthedocs.org `_ +Further documentation can be found at `arrow.readthedocs.io `_. Contributing ------------ -Contributions are welcome, especially with localization. See `locales.py `_ for what's currently supported. +Contributions are welcome for both code and localization. To get started, find an issue or feature to tackle on `the issue tracker `_ and then fork `the repository `_ on GitHub to begin making changes. If you would like to help with localization, please see `locales.py `_ for what locales are currently supported. If you are helping with code, make sure to add tests to ensure that a bug was fixed or the feature works as intended. From 614bf5a2bd3d70f9f9405293bfa1cf98a3ca8210 Mon Sep 17 00:00:00 2001 From: cHYzZQo <37160753+cHYzZQo@users.noreply.github.com> Date: Mon, 13 May 2019 13:26:02 -0700 Subject: [PATCH 158/649] Bump python-dateutil requirement to latest version (#581) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a187d5ac1..054ac02b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -python-dateutil==2.6.0 +python-dateutil==2.8.0 nose==1.3.0 nose-cov==1.6 chai==1.1.1 From 27a95b717dc4779daf687e870c2e7f504af0c847 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Thu, 16 May 2019 11:10:41 +0100 Subject: [PATCH 159/649] Docs update for 0.13.2 --- arrow/arrow.py | 4 ++-- docs/conf.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index f4563d9eb..1aa41492f 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -878,8 +878,8 @@ def is_between(self, start, end, bounds='()'): ''' Returns a boolean denoting whether the specified date and time is between the start and end dates and times. - :param start: an :class:`Arrow `. - :param end: an :class:`Arrow `. + :param start: an :class:`Arrow ` object. + :param end: an :class:`Arrow ` object. :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies whether to include or exclude the start and end values in the range. '(' excludes the start, '[' includes the start, ')' excludes the end, and ']' includes the end. diff --git a/docs/conf.py b/docs/conf.py index 3d9b00438..72eb54113 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.13.0' +version = '0.13.2' # The full version, including alpha/beta/rc tags. -release = '0.13.0' +release = '0.13.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 7f20aefde33eaa67987ebd900b12d1b370e0d254 Mon Sep 17 00:00:00 2001 From: xtreak Date: Thu, 16 May 2019 10:21:31 +0000 Subject: [PATCH 160/649] Fix SyntaxWarning due to escape sequences --- arrow/parser.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index e3a27ea80..b2be6681e 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -21,14 +21,14 @@ class ParserError(RuntimeError): class DateTimeParser(object): _FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|X)') - _ESCAPE_RE = re.compile('\[[^\[\]]*\]') + _ESCAPE_RE = re.compile(r'\[[^\[\]]*\]') - _ONE_OR_MORE_DIGIT_RE = re.compile('\d+') - _ONE_OR_TWO_DIGIT_RE = re.compile('\d{1,2}') - _FOUR_DIGIT_RE = re.compile('\d{4}') - _TWO_DIGIT_RE = re.compile('\d{2}') - _TZ_RE = re.compile('[+\-]?\d{2}:?(\d{2})?') - _TZ_NAME_RE = re.compile('\w[\w+\-/]+') + _ONE_OR_MORE_DIGIT_RE = re.compile(r'\d+') + _ONE_OR_TWO_DIGIT_RE = re.compile(r'\d{1,2}') + _FOUR_DIGIT_RE = re.compile(r'\d{4}') + _TWO_DIGIT_RE = re.compile(r'\d{2}') + _TZ_RE = re.compile(r'[+\-]?\d{2}:?(\d{2})?') + _TZ_NAME_RE = re.compile(r'\w[\w+\-/]+') _BASE_INPUT_RE_MAP = { @@ -46,7 +46,7 @@ class DateTimeParser(object): 'm': _ONE_OR_TWO_DIGIT_RE, 'ss': _TWO_DIGIT_RE, 's': _ONE_OR_TWO_DIGIT_RE, - 'X': re.compile('\d+'), + 'X': re.compile(r'\d+'), 'ZZZ': _TZ_NAME_RE, 'ZZ': _TZ_RE, 'Z': _TZ_RE, @@ -154,7 +154,7 @@ def _generate_pattern_re(self, fmt): offset += len(input_pattern) - (m.end() - m.start()) final_fmt_pattern = "" - a = fmt_pattern.split("\#") + a = fmt_pattern.split(r"\#") b = escaped_data # Due to the way Python splits, 'a' will always be longer @@ -310,7 +310,7 @@ def _choice_re(choices, flags=0): class TzinfoParser(object): - _TZINFO_RE = re.compile('([+\-])?(\d\d):?(\d\d)?') + _TZINFO_RE = re.compile(r'([+\-])?(\d\d):?(\d\d)?') @classmethod def parse(cls, string): From 7a097f236b3ee4202790a454d6b458780f881a33 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Thu, 16 May 2019 11:39:30 +0100 Subject: [PATCH 161/649] Update HISTORY.MD with 0.13.2 --- HISTORY.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 250e4d904..df2b95d83 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,13 @@ ## History +### 0.13.2 + +- [NEW] Add is_between method. +- [FIX] Improved humanize behaviour for near zero durations (#416). +- [FIX] Correct humanize behaviour with future days (#541). +- [FIX] Documentation updates. +- [FIX] Improvements to German Locale. + ### 0.13.1 - [NEW] Add support for Python 3.7. From 910643e67899261c59fb77ddabaa851f7771f87f Mon Sep 17 00:00:00 2001 From: xtreak Date: Fri, 17 May 2019 15:44:47 +0000 Subject: [PATCH 162/649] Add nightly to travis and allow_failures --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 5b5996985..bab967dc2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,10 @@ python: - 3.5 - 3.6 - 3.7 + - nightly +matrix: + allow_failures: + - python: nightly install: - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then make build27; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then make build34; fi From 5292ff4ba697a0f48a0e164fa19f19a9e974be85 Mon Sep 17 00:00:00 2001 From: xtreak Date: Fri, 17 May 2019 15:52:01 +0000 Subject: [PATCH 163/649] Support nightly in makefile --- .travis.yml | 2 +- Makefile | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bab967dc2..9f69a41a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ install: - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then make build34; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then make build35; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then make build36; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.7' ]]; then make build37; fi + - if [[ $TRAVIS_PYTHON_VERSION == 'nightly' ]]; then make build38; fi - pip install codecov script: make test after_success: codecov diff --git a/Makefile b/Makefile index 74f57bacc..f606db746 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,10 @@ build37: virtualenv local --python=python3.7 local/bin/pip install -r requirements.txt +build38: + virtualenv local --python=python3.8 + local/bin/pip install -r requirements.txt + test: rm -f .coverage . local/bin/activate && nosetests From f483405739d0baa9b25fd2c97a5902cf6213869c Mon Sep 17 00:00:00 2001 From: xtreak Date: Fri, 17 May 2019 15:55:10 +0000 Subject: [PATCH 164/649] Add 3.7 back --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 9f69a41a1..643668948 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ install: - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then make build34; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then make build35; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then make build36; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.7' ]]; then make build37; fi - if [[ $TRAVIS_PYTHON_VERSION == 'nightly' ]]; then make build38; fi - pip install codecov script: make test From 2de3ff2e7f2b927fc9425e96e4acb805cf967d34 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sat, 18 May 2019 12:31:06 -0700 Subject: [PATCH 165/649] bump version to 0.13.2 --- arrow/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/__init__.py b/arrow/__init__.py index 23029c79b..761916793 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -4,5 +4,5 @@ from .factory import ArrowFactory from .api import get, now, utcnow -__version__ = '0.13.1' +__version__ = "0.13.2" VERSION = __version__ From a84c591ece7a79f1768d1d1f131d9b8a6745cad0 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sat, 18 May 2019 12:43:10 -0700 Subject: [PATCH 166/649] fix grep in setup.py --- setup.py | 55 ++++++++++++++++++++++++++----------------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/setup.py b/setup.py index 4c1d6d03e..ff12420cd 100644 --- a/setup.py +++ b/setup.py @@ -14,46 +14,43 @@ def fpath(name): def read(fname): - return codecs.open(fpath(fname), encoding='utf-8').read() + return codecs.open(fpath(fname), encoding="utf-8").read() def grep(attrname): - pattern = r"{}\W*=\W*'([^']+)'".format(attrname) + pattern = r'{}\W*=\W*"([^"]+)"'.format(attrname) + print(pattern, file_text) strval, = re.findall(pattern, file_text) return strval -file_text = read(fpath('arrow/__init__.py')) +file_text = read(fpath("arrow/__init__.py")) setup( - name='arrow', - version=grep('__version__'), - description='Better dates and times for Python', - long_description=read(fpath('README.rst')), - url='https://github.com/crsmithdev/arrow/', - author='Chris Smith', + name="arrow", + version=grep("__version__"), + description="Better dates and times for Python", + long_description=read(fpath("README.rst")), + url="https://github.com/crsmithdev/arrow/", + author="Chris Smith", author_email="crsmithdev@gmail.com", - license='Apache 2.0', - packages=['arrow'], + license="Apache 2.0", + packages=["arrow"], zip_safe=False, - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*', - install_requires=[ - 'python-dateutil', - ], - extras_require={ - ":python_version=='2.7'": ['backports.functools_lru_cache>=1.2.1'], - }, + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*", + install_requires=["python-dateutil"], + extras_require={":python_version=='2.7'": ["backports.functools_lru_cache>=1.2.1"]}, test_suite="tests", - tests_require=['chai'], + tests_require=["chai"], classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Topic :: Software Development :: Libraries :: Python Modules' - ] + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Topic :: Software Development :: Libraries :: Python Modules", + ], ) From 3fc3233c0918b7230e58320051da4b0f5cdff3e7 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sat, 18 May 2019 12:47:28 -0700 Subject: [PATCH 167/649] remove print in setup.py --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index ff12420cd..11c7a2567 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,6 @@ def read(fname): def grep(attrname): pattern = r'{}\W*=\W*"([^"]+)"'.format(attrname) - print(pattern, file_text) strval, = re.findall(pattern, file_text) return strval From 556e7a55e03ca917914a8106c19cc1d82a074c49 Mon Sep 17 00:00:00 2001 From: xtreak Date: Mon, 20 May 2019 18:01:33 +0000 Subject: [PATCH 168/649] Use 3.8-dev --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 643668948..7b39d4809 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - 3.5 - 3.6 - 3.7 - - nightly + - 3.8-dev matrix: allow_failures: - python: nightly @@ -17,7 +17,7 @@ install: - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then make build35; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then make build36; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.7' ]]; then make build37; fi - - if [[ $TRAVIS_PYTHON_VERSION == 'nightly' ]]; then make build38; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.8-dev' ]]; then make build38; fi - pip install codecov script: make test after_success: codecov From b7dfed1d43877d8c6b6c06206143c3300e67a109 Mon Sep 17 00:00:00 2001 From: xtreak Date: Mon, 20 May 2019 18:10:29 +0000 Subject: [PATCH 169/649] Rename nightly to 3.8-dev --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7b39d4809..799c1b30a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ python: - 3.8-dev matrix: allow_failures: - - python: nightly + - python: 3.8-dev install: - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then make build27; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then make build34; fi From c96ada19c70b9afa73577450bf574a82a71da793 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 21 May 2019 05:52:39 -0400 Subject: [PATCH 170/649] Deprecated support for Python 3.4 as it has reached end-of-life status (#588) --- .travis.yml | 2 -- Makefile | 4 ---- README.rst | 2 +- setup.py | 3 +-- tox.ini | 2 +- 5 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 799c1b30a..84ecfd76a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ language: python dist: xenial python: - 2.7 - - 3.4 - 3.5 - 3.6 - 3.7 @@ -13,7 +12,6 @@ matrix: - python: 3.8-dev install: - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then make build27; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then make build34; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then make build35; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then make build36; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.7' ]]; then make build37; fi diff --git a/Makefile b/Makefile index f606db746..58d1bcdfd 100644 --- a/Makefile +++ b/Makefile @@ -6,10 +6,6 @@ build27: virtualenv local --python=python2.7 local/bin/pip install -r requirements.txt -build34: - virtualenv local --python=python3.4 - local/bin/pip install -r requirements.txt - build35: virtualenv local --python=python3.5 local/bin/pip install -r requirements.txt diff --git a/README.rst b/README.rst index 2a43ad5ce..6b56b9e92 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ Features -------- - Fully implemented, drop-in replacement for datetime -- Supports Python 2.7, 3.4, 3.5, 3.6 and 3.7 +- Supports Python 2.7, 3.5, 3.6 and 3.7 - Time zone-aware & UTC by default - Provides super-simple creation options for many common input scenarios - Updated .replace method with support for relative offsets, including weeks diff --git a/setup.py b/setup.py index 11c7a2567..966a294a8 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ def grep(attrname): license="Apache 2.0", packages=["arrow"], zip_safe=False, - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*", + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", install_requires=["python-dateutil"], extras_require={":python_version=='2.7'": ["backports.functools_lru_cache>=1.2.1"]}, test_suite="tests", @@ -46,7 +46,6 @@ def grep(attrname): "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", diff --git a/tox.ini b/tox.ini index 50e6fc93e..fb74896fa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36,py37 +envlist = py27,py35,py36,py37 skip_missing_interpreters = True [common] From 470d7e46e464898563f59257b5ee69f51debedf7 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 25 May 2019 06:00:28 -0400 Subject: [PATCH 171/649] Added badges and updated README (#590) * Added extra badges and improved quick start section * Added upgrade tag to pip install * Made quick start two words * Added 3.8 to README and setup.py and updated pip link * Added pipenv reference --- README.rst | 24 ++++++++++++++++++------ setup.py | 1 + 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 6b56b9e92..45acfac6c 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,15 @@ Arrow - Better dates & times for Python .. image:: https://img.shields.io/pypi/v/arrow.svg :target: https://pypi.python.org/pypi/arrow - :alt: downloads + :alt: arrow PyPI download + +.. image:: https://img.shields.io/pypi/pyversions/arrow.svg + :target: https://pypi.python.org/pypi/arrow + :alt: python versions + +.. image:: https://img.shields.io/pypi/l/arrow.svg + :target: https://pypi.python.org/pypi/arrow + :alt: license Documentation: `arrow.readthedocs.io `_ --------------------------------------------------------------------------------- @@ -38,7 +46,7 @@ Features -------- - Fully implemented, drop-in replacement for datetime -- Supports Python 2.7, 3.5, 3.6 and 3.7 +- Supports Python 2.7, 3.5, 3.6, 3.7, and 3.8 - Time zone-aware & UTC by default - Provides super-simple creation options for many common input scenarios - Updated .replace method with support for relative offsets, including weeks @@ -50,16 +58,20 @@ Features - Humanizes and supports a growing list of contributed locales - Extensible for your own Arrow-derived types -Quick start +Quick Start ----------- -First: +Installation +^^^^^^^^^^^^ + +To install Arrow, use `pip `_ or `pipenv `_: .. code-block:: console - $ pip install arrow + $ pip install -U arrow -And then: +Example Usage +^^^^^^^^^^^^^ .. code-block:: pycon diff --git a/setup.py b/setup.py index 966a294a8..2447d6838 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,7 @@ def grep(attrname): "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Topic :: Software Development :: Libraries :: Python Modules", ], ) From 32c4cf097ca00bb5d16b683c6da7fd1217f2c15f Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 25 May 2019 13:37:29 -0400 Subject: [PATCH 172/649] Upgraded dependencies in testing environment to latest version --- requirements.txt | 8 ++++---- setup.cfg | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 054ac02b0..bf5f6c6d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ python-dateutil==2.8.0 -nose==1.3.0 +nose==1.3.7 nose-cov==1.6 -chai==1.1.1 +chai==1.1.2 sphinx==1.3.5 -simplejson==3.6.5 -backports.functools_lru_cache==1.2.1 +simplejson==3.16.0 +backports.functools_lru_cache==1.5.0 diff --git a/setup.cfg b/setup.cfg index 53e78d1e2..c4d614dc1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,9 +5,7 @@ verbosity = 2 all-modules = true with-coverage = true cover-min-percentage = 100 -cover-package = - arrow - tests +cover-package = arrow cover-erase = true [bdist_wheel] From 8b866792570af8076559a4a840dd9b00dda3a57f Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 25 May 2019 14:57:58 -0400 Subject: [PATCH 173/649] Updated sphinx Makefile and conf.py based on latest sphinx-quickstart --- docs/Makefile | 176 ++------------------------------- docs/conf.py | 250 ++++++----------------------------------------- requirements.txt | 2 +- 3 files changed, 39 insertions(+), 389 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index f1957cab9..298ea9e21 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,177 +1,19 @@ -# Makefile for Sphinx documentation +# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -PAPER = +SOURCEDIR = . BUILDDIR = _build -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - +# Put it first so that "make" without argument is like "make help". help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Arrow.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Arrow.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/Arrow" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Arrow" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." +.PHONY: help Makefile -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 72eb54113..3c3ddd0d5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,250 +1,58 @@ -# -*- coding: utf-8 -*- +# Configuration file for the Sphinx documentation builder. # -# Arrow documentation build configuration file, created by -# sphinx-quickstart on Mon May 6 15:25:39 2013. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# http://www.sphinx-doc.org/en/master/config -import sys, os +# -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../')) - -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc'] +import os +import sys +sys.path.insert(0, os.path.abspath('..')) -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +# -- Project information ----------------------------------------------------- -# The suffix of source filenames. -source_suffix = '.rst' +project = 'arrow' +copyright = '2019, Chris Smith' +author = 'Chris Smith' -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'Arrow' -copyright = u'2019, Chris Smith' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '0.13.2' -# The full version, including alpha/beta/rc tags. +# The full version, including alpha/beta/rc tags release = '0.13.2' -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None +# -- General configuration --------------------------------------------------- -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build', '_themes'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. +master_doc = 'index' +source_suffix = '.rst' pygments_style = 'sphinx' -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - - -# -- Options for HTML output --------------------------------------------------- +# -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'f6' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} -# Add any paths that contain custom themes here, relative to this directory. +html_theme = 'f6' html_theme_path = ['_themes'] -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -html_use_index = False - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -html_show_sourcelink = False - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'Arrowdoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'Arrow.tex', u'Arrow Documentation', - u'Chris Smith', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'arrow', u'Arrow Documentation', - [u'Chris Smith'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------------ - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'Arrow', u'Arrow Documentation', - u'Chris Smith', 'Arrow', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - -autodoc_member_order = 'bysource' diff --git a/requirements.txt b/requirements.txt index bf5f6c6d7..cb42a3f56 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,6 @@ python-dateutil==2.8.0 nose==1.3.7 nose-cov==1.6 chai==1.1.2 -sphinx==1.3.5 +sphinx==2.0.1 simplejson==3.16.0 backports.functools_lru_cache==1.5.0 From d3586171f5d43eb7b7fab620998fc6b7423caa27 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 25 May 2019 15:07:52 -0400 Subject: [PATCH 174/649] Removed sphinx from requirements.txt since it is not needed in Travis builds --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cb42a3f56..900b2f7d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,5 @@ python-dateutil==2.8.0 nose==1.3.7 nose-cov==1.6 chai==1.1.2 -sphinx==2.0.1 simplejson==3.16.0 backports.functools_lru_cache==1.5.0 From 486941ae4d355c5b9049867160ab37811ab74ca0 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 25 May 2019 15:14:41 -0400 Subject: [PATCH 175/649] added pytz as dependency for tests --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 900b2f7d0..60daf2604 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ python-dateutil==2.8.0 +pytz==2019.1 nose==1.3.7 nose-cov==1.6 chai==1.1.2 From 2e5031b473bcf10b99b9d7ff8a7e72499c412c07 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 26 May 2019 06:26:01 -0400 Subject: [PATCH 176/649] Updated setup.py with modern Python standards (#592) --- arrow/__init__.py | 1 - setup.py | 32 ++++++-------------------------- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/arrow/__init__.py b/arrow/__init__.py index 761916793..e486ba287 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -5,4 +5,3 @@ from .api import get, now, utcnow __version__ = "0.13.2" -VERSION = __version__ diff --git a/setup.py b/setup.py index 2447d6838..19c513d06 100644 --- a/setup.py +++ b/setup.py @@ -1,35 +1,15 @@ -import codecs -import os.path -import re -import sys +from setuptools import setup -try: - from setuptools import setup -except ImportError: - from distutils.core import setup +from arrow import __version__ - -def fpath(name): - return os.path.join(os.path.dirname(__file__), name) - - -def read(fname): - return codecs.open(fpath(fname), encoding="utf-8").read() - - -def grep(attrname): - pattern = r'{}\W*=\W*"([^"]+)"'.format(attrname) - strval, = re.findall(pattern, file_text) - return strval - - -file_text = read(fpath("arrow/__init__.py")) +with open("README.rst", "r") as f: + readme = f.read() setup( name="arrow", - version=grep("__version__"), + version=__version__, description="Better dates and times for Python", - long_description=read(fpath("README.rst")), + long_description=readme, url="https://github.com/crsmithdev/arrow/", author="Chris Smith", author_email="crsmithdev@gmail.com", From 725f019451f827e7d565d721001ef31d40254b2e Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 26 May 2019 12:54:47 -0400 Subject: [PATCH 177/649] Exclude README documents in _themes folder --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 3c3ddd0d5..6a3d18e59 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,7 +38,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '_themes/README.rst', '_themes/f6/README.rst'] master_doc = 'index' source_suffix = '.rst' From 7f9430bc7578dbd0b5288588fa2de6855d87312d Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 26 May 2019 15:06:42 -0400 Subject: [PATCH 178/649] Adjusted setup.py to help improve visibility on PyPI --- setup.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 19c513d06..d908474e7 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,8 @@ version=__version__, description="Better dates and times for Python", long_description=readme, - url="https://github.com/crsmithdev/arrow/", + long_description_content_type="text/x-rst", + url="https://arrow.readthedocs.io/en/latest/", author="Chris Smith", author_email="crsmithdev@gmail.com", license="Apache 2.0", @@ -25,11 +26,21 @@ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", + "Topic :: Software Development :: Libraries :: Python Modules", + "Operating System :: POSIX", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS :: MacOS X", + "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", - "Topic :: Software Development :: Libraries :: Python Modules", ], + keywords="arrow time date datetime locale moment", + project_urls={ + "Bug Reports": "https://github.com/crsmithdev/arrow/issues", + "Source": "https://github.com/crsmithdev/arrow", + }, ) From dd10d4f109a738fed64b23ae44bd9a8048849cf3 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 26 May 2019 15:37:49 -0400 Subject: [PATCH 179/649] Tweaked setup.py classifiers and project URLs --- setup.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index d908474e7..9935d1325 100644 --- a/setup.py +++ b/setup.py @@ -27,9 +27,6 @@ "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Topic :: Software Development :: Libraries :: Python Modules", - "Operating System :: POSIX", - "Operating System :: Microsoft :: Windows", - "Operating System :: MacOS :: MacOS X", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", @@ -40,7 +37,7 @@ ], keywords="arrow time date datetime locale moment", project_urls={ - "Bug Reports": "https://github.com/crsmithdev/arrow/issues", - "Source": "https://github.com/crsmithdev/arrow", + "GitHub": "https://github.com/crsmithdev/arrow", + "Bug Reports": "https://github.com/crsmithdev/arrow/issues" }, ) From 7b2e746bea0dc556d80976188fdc785bb0ecf0ed Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 26 May 2019 15:39:14 -0400 Subject: [PATCH 180/649] Adjusted keywords --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 9935d1325..18a7b1a6e 100644 --- a/setup.py +++ b/setup.py @@ -35,9 +35,9 @@ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", ], - keywords="arrow time date datetime locale moment", + keywords="arrow date time datetime", project_urls={ - "GitHub": "https://github.com/crsmithdev/arrow", + "Repository": "https://github.com/crsmithdev/arrow", "Bug Reports": "https://github.com/crsmithdev/arrow/issues" }, ) From eb28782a96da8aadd3fa1dfff3ff955d91416f31 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 28 May 2019 17:31:35 -0400 Subject: [PATCH 181/649] Added support for sphinx in virtualenv --- Makefile | 2 +- requirements.txt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 58d1bcdfd..fe9127c1f 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ test: docs: touch docs/index.rst - cd docs; make html + . local/bin/activate && cd docs; make html clean: rm -rf local diff --git a/requirements.txt b/requirements.txt index 60daf2604..cf9186581 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,6 @@ nose==1.3.7 nose-cov==1.6 chai==1.1.2 simplejson==3.16.0 +sphinx==1.8.5; python_version == '2.7' +sphinx==2.0.1; python_version >= '3.5' backports.functools_lru_cache==1.5.0 From 606b03cb7375fd4fcd2ff3d128ea2fbde191bb95 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Wed, 29 May 2019 19:23:18 -0400 Subject: [PATCH 182/649] Added flake8 to enforce formatting and style requirements (#587) * Made package and tests flake8 compliant; reformatted code using black. * Renamed target in Makefile * Added missing regex r to string * Moved .coveragerc settings into setup.cfg to centralize settings * Tweaked makefile targets and added black and isort to travis pipeline * Reverted adding black and isort to travis pipeline for compatibility reasons * Added code style black badge * Added flake8, black, and miscallaneous pre-commit hooks * Fixed makefile to install pre commit hooks * Run pre-commit globally on Travis * Attempt python 2 fix * Added linting run only on Python 3.7 * added global pip * Added isort pre-commit hook * Added remaining hooks for checking rst backticks and eval * Added initialization of pre-commit to all build targets * Added python2.7 as Black target for pre-commit * Changed blanket except to except: Exception to prevent catching unintentional errors like memory errors * Added flake8 plugins (bugbear and sort imports) to further improve code style * Attempt running linting checks on Python 2 * Configure pyenv to use 3.7 * More debug info and force py37 on pre-commit * Revamped Travis config to accomodate a separate target for linting based on feedback from @asottile * Removed pip cache and added labeling * Renamed section of Travis * Put Travis run script on single line * Added fast finishing and stages to Travis build so that linting will go before tests * Small tweaks to travis and flake8 configs * Removed duplicate isort cfg * Addressed Anthony's feedback * Updated pre-commit dependency to fix occasional FileNotFoundError * Added back pre-commit to requirements.txt--it got lost in the merge --- .coveragerc | 9 - .pre-commit-config.yaml | 39 + .travis.yml | 27 +- Makefile | 19 +- README.rst | 4 + arrow/__init__.py | 2 +- arrow/api.py | 27 +- arrow/arrow.py | 512 ++-- arrow/factory.py | 58 +- arrow/formatter.py | 105 +- arrow/locales.py | 4433 ++++++++++++++++++++++++----------- arrow/parser.py | 277 ++- arrow/util.py | 62 +- docs/Makefile | 2 +- docs/_themes/COPYING.txt | 1 - docs/_themes/f6/layout.html | 2 +- docs/conf.py | 36 +- docs/index.rst | 2 +- requirements.txt | 9 +- setup.cfg | 20 +- setup.py | 3 +- tests/api_tests.py | 25 +- tests/arrow_tests.py | 1646 +++++++------ tests/factory_tests.py | 149 +- tests/formatter_tests.py | 136 +- tests/locales_tests.py | 748 +++--- tests/parser_tests.py | 836 +++---- 27 files changed, 5723 insertions(+), 3466 deletions(-) delete mode 100644 .coveragerc create mode 100644 .pre-commit-config.yaml diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 1e8b639e2..000000000 --- a/.coveragerc +++ /dev/null @@ -1,9 +0,0 @@ -[run] -branch = True -source = - tests - arrow - -[report] -show_missing = True -fail_under = 100 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..f82c849ab --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +default_language_version: + python: python3 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.2.3 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: fix-encoding-pragma + - id: check-yaml + - id: debug-statements + - id: requirements-txt-fixer + - repo: https://github.com/asottile/seed-isort-config + rev: v1.9.1 + hooks: + - id: seed-isort-config + - repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.20 + hooks: + - id: isort + - repo: https://github.com/asottile/pyupgrade + rev: v1.17.1 + hooks: + - id: pyupgrade + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.4.0 + hooks: + - id: python-no-eval + - id: rst-backticks + - repo: https://github.com/python/black + rev: 19.3b0 + hooks: + - id: black + args: [--safe, --quiet, --target-version=py27] + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.7 + hooks: + - id: flake8 + additional_dependencies: [flake8-bugbear] diff --git a/.travis.yml b/.travis.yml index 84ecfd76a..16b0f81b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,21 +1,26 @@ -sudo: false language: python dist: xenial -python: - - 2.7 - - 3.5 - - 3.6 - - 3.7 - - 3.8-dev matrix: - allow_failures: - - python: 3.8-dev + include: + - python: 2.7 + - python: 3.5 + - python: 3.6 + - python: 3.7 + - python: 3.8-dev + - name: "Linting" + python: 3.7 + env: TARGET=lint-ci + cache: + directories: + - $HOME/.cache/pre-commit + allow_failures: + - python: 3.8-dev +before_install: pip install -U codecov install: - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then make build27; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then make build35; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then make build36; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.7' ]]; then make build37; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.8-dev' ]]; then make build38; fi - - pip install codecov -script: make test +script: make "${TARGET:-test}" after_success: codecov diff --git a/Makefile b/Makefile index fe9127c1f..de2167ff0 100644 --- a/Makefile +++ b/Makefile @@ -1,37 +1,46 @@ -.PHONY: auto build test docs clean +.PHONY: auto test docs clean auto: build27 build27: virtualenv local --python=python2.7 local/bin/pip install -r requirements.txt + local/bin/pre-commit install build35: virtualenv local --python=python3.5 local/bin/pip install -r requirements.txt + local/bin/pre-commit install build36: virtualenv local --python=python3.6 local/bin/pip install -r requirements.txt + local/bin/pre-commit install build37: virtualenv local --python=python3.7 local/bin/pip install -r requirements.txt + local/bin/pre-commit install build38: virtualenv local --python=python3.8 local/bin/pip install -r requirements.txt + local/bin/pre-commit install test: rm -f .coverage . local/bin/activate && nosetests +lint: + local/bin/pre-commit run --all-files + +lint-ci: + local/bin/pre-commit run --all-files --show-diff-on-failure + docs: touch docs/index.rst . local/bin/activate && cd docs; make html clean: - rm -rf local - rm -f arrow/*.pyc tests/*.pyc - rm -f .coverage - + rm -rf local ./**/__pycache__ + rm -f ./**/*.pyc .coverage diff --git a/README.rst b/README.rst index 45acfac6c..113a6d305 100644 --- a/README.rst +++ b/README.rst @@ -21,6 +21,10 @@ Arrow - Better dates & times for Python :target: https://pypi.python.org/pypi/arrow :alt: license +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/python/black + :alt: code style + Documentation: `arrow.readthedocs.io `_ --------------------------------------------------------------------------------- diff --git a/arrow/__init__.py b/arrow/__init__.py index e486ba287..9d2b85293 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- +from .api import get, now, utcnow from .arrow import Arrow from .factory import ArrowFactory -from .api import get, now, utcnow __version__ = "0.13.2" diff --git a/arrow/api.py b/arrow/api.py index f23f27398..c13fd7144 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -1,57 +1,60 @@ # -*- coding: utf-8 -*- -''' +""" Provides the default implementation of :class:`ArrowFactory ` methods for use as a module API. -''' +""" from __future__ import absolute_import from arrow.factory import ArrowFactory - # internal default factory. _factory = ArrowFactory() def get(*args, **kwargs): - ''' Calls the default :class:`ArrowFactory ` ``get`` method. + """ Calls the default :class:`ArrowFactory ` ``get`` method. - ''' + """ return _factory.get(*args, **kwargs) + get.__doc__ = _factory.get.__doc__ def utcnow(): - ''' Calls the default :class:`ArrowFactory ` ``utcnow`` method. + """ Calls the default :class:`ArrowFactory ` ``utcnow`` method. - ''' + """ return _factory.utcnow() + utcnow.__doc__ = _factory.utcnow.__doc__ def now(tz=None): - ''' Calls the default :class:`ArrowFactory ` ``now`` method. + """ Calls the default :class:`ArrowFactory ` ``now`` method. - ''' + """ return _factory.now(tz) + now.__doc__ = _factory.now.__doc__ def factory(type): - ''' Returns an :class:`.ArrowFactory` for the specified :class:`Arrow ` + """ Returns an :class:`.ArrowFactory` for the specified :class:`Arrow ` or derived type. :param type: the type, :class:`Arrow ` or derived. - ''' + """ return ArrowFactory(type) -__all__ = ['get', 'utcnow', 'now', 'factory'] + +__all__ = ["get", "utcnow", "now", "factory"] diff --git a/arrow/arrow.py b/arrow/arrow.py index 1aa41492f..19bbbe0b4 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1,26 +1,26 @@ # -*- coding: utf-8 -*- -''' +""" Provides the :class:`Arrow ` class, an enhanced ``datetime`` replacement. -''' +""" from __future__ import absolute_import -from datetime import datetime, timedelta, tzinfo -from dateutil import tz as dateutil_tz -from dateutil.relativedelta import relativedelta -from math import trunc import calendar import sys import warnings +from datetime import datetime, timedelta, tzinfo +from math import trunc +from dateutil import tz as dateutil_tz +from dateutil.relativedelta import relativedelta -from arrow import util, locales, parser, formatter +from arrow import formatter, locales, parser, util class Arrow(object): - '''An :class:`Arrow ` object. + """An :class:`Arrow ` object. Implements the ``datetime`` interface, behaving as an aware ``datetime`` while implementing additional functionality. @@ -49,30 +49,31 @@ class Arrow(object): >>> arrow.Arrow(2013, 5, 5, 12, 30, 45) - ''' + """ resolution = datetime.resolution - _ATTRS = ['year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond'] - _ATTRS_PLURAL = ['{}s'.format(a) for a in _ATTRS] + _ATTRS = ["year", "month", "day", "hour", "minute", "second", "microsecond"] + _ATTRS_PLURAL = ["{}s".format(a) for a in _ATTRS] _MONTHS_PER_QUARTER = 3 - def __init__(self, year, month, day, hour=0, minute=0, second=0, microsecond=0, - tzinfo=None): + def __init__( + self, year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None + ): if util.isstr(tzinfo): tzinfo = parser.TzinfoParser.parse(tzinfo) tzinfo = tzinfo if tzinfo is not None else dateutil_tz.tzutc() - self._datetime = datetime(year, month, day, hour, minute, second, - microsecond, tzinfo) - + self._datetime = datetime( + year, month, day, hour, minute, second, microsecond, tzinfo + ) # factories: single object, both original and from datetime. @classmethod def now(cls, tzinfo=None): - '''Constructs an :class:`Arrow ` object, representing "now" in the given + """Constructs an :class:`Arrow ` object, representing "now" in the given timezone. :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. @@ -82,17 +83,25 @@ def now(cls, tzinfo=None): >>> arrow.now('Asia/Baku') - ''' + """ tzinfo = tzinfo if tzinfo is not None else dateutil_tz.tzlocal() dt = datetime.now(tzinfo) - return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, dt.tzinfo) + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + dt.tzinfo, + ) @classmethod def utcnow(cls): - ''' Constructs an :class:`Arrow ` object, representing "now" in UTC + """ Constructs an :class:`Arrow ` object, representing "now" in UTC time. Usage:: @@ -100,16 +109,24 @@ def utcnow(cls): >>> arrow.utcnow() - ''' + """ dt = datetime.now(dateutil_tz.tzutc()) - return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, dt.tzinfo) + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + dt.tzinfo, + ) @classmethod def fromtimestamp(cls, timestamp, tzinfo=None): - ''' Constructs an :class:`Arrow ` object from a timestamp, converted to + """ Constructs an :class:`Arrow ` object from a timestamp, converted to the given timezone. :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. @@ -120,32 +137,48 @@ def fromtimestamp(cls, timestamp, tzinfo=None): >>> arrow.Arrow.utcfromtimestamp(1367900664).replace(tzinfo='US/Pacific') - ''' + """ tzinfo = tzinfo if tzinfo is not None else dateutil_tz.tzlocal() timestamp = cls._get_timestamp_from_input(timestamp) dt = datetime.fromtimestamp(timestamp, tzinfo) - return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, dt.tzinfo) + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + dt.tzinfo, + ) @classmethod def utcfromtimestamp(cls, timestamp): - '''Constructs an :class:`Arrow ` object from a timestamp, in UTC time. + """Constructs an :class:`Arrow ` object from a timestamp, in UTC time. :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. - ''' + """ timestamp = cls._get_timestamp_from_input(timestamp) dt = datetime.utcfromtimestamp(timestamp) - return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, dateutil_tz.tzutc()) + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + dateutil_tz.tzutc(), + ) @classmethod def fromdatetime(cls, dt, tzinfo=None): - ''' Constructs an :class:`Arrow ` object from a ``datetime`` and + """ Constructs an :class:`Arrow ` object from a ``datetime`` and optional replacement timezone. :param dt: the ``datetime`` @@ -159,7 +192,7 @@ def fromdatetime(cls, dt, tzinfo=None): >>> arrow.Arrow.fromdatetime(dt, dt.tzinfo or 'US/Pacific') - ''' + """ if tzinfo is None: if dt.tzinfo is None: @@ -167,17 +200,25 @@ def fromdatetime(cls, dt, tzinfo=None): else: tzinfo = dt.tzinfo - return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, tzinfo) + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo, + ) @classmethod def fromdate(cls, date, tzinfo=None): - ''' Constructs an :class:`Arrow ` object from a ``date`` and optional + """ Constructs an :class:`Arrow ` object from a ``date`` and optional replacement timezone. Time values are set to 0. :param date: the ``date`` :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to UTC. - ''' + """ tzinfo = tzinfo if tzinfo is not None else dateutil_tz.tzutc() @@ -185,7 +226,7 @@ def fromdate(cls, date, tzinfo=None): @classmethod def strptime(cls, date_str, fmt, tzinfo=None): - ''' Constructs an :class:`Arrow ` object from a date string and format, + """ Constructs an :class:`Arrow ` object from a date string and format, in the style of ``datetime.strptime``. Optionally replaces the parsed timezone. :param date_str: the date string. @@ -198,20 +239,27 @@ def strptime(cls, date_str, fmt, tzinfo=None): >>> arrow.Arrow.strptime('20-01-2019 15:49:10', '%d-%m-%Y %H:%M:%S') - ''' + """ dt = datetime.strptime(date_str, fmt) tzinfo = tzinfo if tzinfo is not None else dt.tzinfo - return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, tzinfo) - + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo, + ) # factories: ranges and spans @classmethod def range(cls, frame, start, end=None, tz=None, limit=None): - ''' Returns an iterator of :class:`Arrow ` objects, representing + """ Returns an iterator of :class:`Arrow ` objects, representing points in time between two inputs. :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). @@ -259,7 +307,7 @@ def range(cls, frame, start, end=None, tz=None, limit=None): - ''' + """ _, frame_relative, relative_steps = cls._get_frames(frame) @@ -277,12 +325,13 @@ def range(cls, frame, start, end=None, tz=None, limit=None): yield current values = [getattr(current, f) for f in cls._ATTRS] - current = cls(*values, tzinfo=tzinfo) + relativedelta(**{frame_relative: relative_steps}) - + current = cls(*values, tzinfo=tzinfo) + relativedelta( + **{frame_relative: relative_steps} + ) @classmethod def span_range(cls, frame, start, end, tz=None, limit=None): - ''' Returns an iterator of tuples, each :class:`Arrow ` objects, + """ Returns an iterator of tuples, each :class:`Arrow ` objects, representing a series of timespans between two inputs. :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). @@ -324,7 +373,7 @@ def span_range(cls, frame, start, end, tz=None, limit=None): (, ) (, ) - ''' + """ tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) start = cls.fromdatetime(start, tzinfo).span(frame)[0] @@ -333,7 +382,7 @@ def span_range(cls, frame, start, end, tz=None, limit=None): @classmethod def interval(cls, frame, start, end, interval=1, tz=None): - ''' Returns an iterator of tuples, each :class:`Arrow ` objects, + """ Returns an iterator of tuples, each :class:`Arrow ` objects, representing a series of intervals between two inputs. :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). @@ -366,7 +415,7 @@ def interval(cls, frame, start, end, interval=1, tz=None): (, ) (, ) (, ) - ''' + """ if interval < 1: raise ValueError("interval has to be a positive integer") @@ -374,7 +423,7 @@ def interval(cls, frame, start, end, interval=1, tz=None): while True: try: intvlStart, intvlEnd = next(spanRange) - for _ in range(interval-1): + for _ in range(interval - 1): _, intvlEnd = next(spanRange) yield intvlStart, intvlEnd except StopIteration: @@ -383,7 +432,7 @@ def interval(cls, frame, start, end, interval=1, tz=None): # representations def __repr__(self): - return '<{} [{}]>'.format(self.__class__.__name__, self.__str__()) + return "<{} [{}]>".format(self.__class__.__name__, self.__str__()) def __str__(self): return self._datetime.isoformat() @@ -398,18 +447,17 @@ def __format__(self, formatstr): def __hash__(self): return self._datetime.__hash__() - # attributes & properties def __getattr__(self, name): - if name == 'week': + if name == "week": return self.isocalendar()[1] - if name == 'quarter': - return int((self.month-1)/self._MONTHS_PER_QUARTER) + 1 + if name == "quarter": + return int((self.month - 1) / self._MONTHS_PER_QUARTER) + 1 - if not name.startswith('_'): + if not name.startswith("_"): value = getattr(self._datetime, name, None) if value is not None: @@ -419,7 +467,7 @@ def __getattr__(self, name): @property def tzinfo(self): - ''' Gets the ``tzinfo`` of the :class:`Arrow ` object. + """ Gets the ``tzinfo`` of the :class:`Arrow ` object. Usage:: @@ -427,19 +475,19 @@ def tzinfo(self): >>> arw.tzinfo tzutc() - ''' + """ return self._datetime.tzinfo @tzinfo.setter def tzinfo(self, tzinfo): - ''' Sets the ``tzinfo`` of the :class:`Arrow ` object. ''' + """ Sets the ``tzinfo`` of the :class:`Arrow ` object. """ self._datetime = self._datetime.replace(tzinfo=tzinfo) @property def datetime(self): - ''' Returns a datetime representation of the :class:`Arrow ` object. + """ Returns a datetime representation of the :class:`Arrow ` object. Usage:: @@ -447,13 +495,13 @@ def datetime(self): >>> arw.datetime datetime.datetime(2019, 1, 24, 16, 35, 27, 276649, tzinfo=tzutc()) - ''' + """ return self._datetime @property def naive(self): - ''' Returns a naive datetime representation of the :class:`Arrow ` + """ Returns a naive datetime representation of the :class:`Arrow ` object. Usage:: @@ -464,13 +512,13 @@ def naive(self): >>> nairobi.naive datetime.datetime(2019, 1, 23, 19, 27, 12, 297999) - ''' + """ return self._datetime.replace(tzinfo=None) @property def timestamp(self): - ''' Returns a timestamp representation of the :class:`Arrow ` object, in + """ Returns a timestamp representation of the :class:`Arrow ` object, in UTC time. Usage:: @@ -478,13 +526,13 @@ def timestamp(self): >>> arrow.utcnow().timestamp 1548260567 - ''' + """ return calendar.timegm(self._datetime.utctimetuple()) @property def float_timestamp(self): - ''' Returns a floating-point representation of the :class:`Arrow ` + """ Returns a floating-point representation of the :class:`Arrow ` object, in UTC time. Usage:: @@ -492,27 +540,26 @@ def float_timestamp(self): >>> arrow.utcnow().float_timestamp 1548260516.830896 - ''' + """ return self.timestamp + float(self.microsecond) / 1000000 - # mutation and duplication. def clone(self): - ''' Returns a new :class:`Arrow ` object, cloned from the current one. + """ Returns a new :class:`Arrow ` object, cloned from the current one. Usage: >>> arw = arrow.utcnow() >>> cloned = arw.clone() - ''' + """ return self.fromdatetime(self._datetime) def replace(self, **kwargs): - ''' Returns a new :class:`Arrow ` object with attributes updated + """ Returns a new :class:`Arrow ` object with attributes updated according to inputs. Use property names to set their value absolutely:: @@ -530,7 +577,7 @@ def replace(self, **kwargs): >>> arw.replace(tzinfo=tz.tzlocal()) - ''' + """ absolute_kwargs = {} relative_kwargs = {} # TODO: DEPRECATED; remove in next release @@ -539,25 +586,29 @@ def replace(self, **kwargs): if key in self._ATTRS: absolute_kwargs[key] = value - elif key in self._ATTRS_PLURAL or key in ['weeks', 'quarters']: + elif key in self._ATTRS_PLURAL or key in ["weeks", "quarters"]: # TODO: DEPRECATED - warnings.warn("replace() with plural property to shift value " - "is deprecated, use shift() instead", - DeprecationWarning) + warnings.warn( + "replace() with plural property to shift value " + "is deprecated, use shift() instead", + DeprecationWarning, + ) relative_kwargs[key] = value - elif key in ['week', 'quarter']: - raise AttributeError('setting absolute {} is not supported'.format(key)) - elif key !='tzinfo': + elif key in ["week", "quarter"]: + raise AttributeError("setting absolute {} is not supported".format(key)) + elif key != "tzinfo": raise AttributeError('unknown attribute: "{}"'.format(key)) # core datetime does not support quarters, translate to months. - relative_kwargs.setdefault('months', 0) - relative_kwargs['months'] += relative_kwargs.pop('quarters', 0) * self._MONTHS_PER_QUARTER + relative_kwargs.setdefault("months", 0) + relative_kwargs["months"] += ( + relative_kwargs.pop("quarters", 0) * self._MONTHS_PER_QUARTER + ) current = self._datetime.replace(**absolute_kwargs) - current += relativedelta(**relative_kwargs) # TODO: DEPRECATED + current += relativedelta(**relative_kwargs) # TODO: DEPRECATED - tzinfo = kwargs.get('tzinfo') + tzinfo = kwargs.get("tzinfo") if tzinfo is not None: tzinfo = self._get_tzinfo(tzinfo) @@ -566,7 +617,7 @@ def replace(self, **kwargs): return self.fromdatetime(current) def shift(self, **kwargs): - ''' Returns a new :class:`Arrow ` object with attributes updated + """ Returns a new :class:`Arrow ` object with attributes updated according to inputs. Use pluralized property names to shift their current value relatively: @@ -593,27 +644,29 @@ def shift(self, **kwargs): >>> arw.shift(weekday=0) - ''' + """ relative_kwargs = {} for key, value in kwargs.items(): - if key in self._ATTRS_PLURAL or key in ['weeks', 'quarters', 'weekday']: + if key in self._ATTRS_PLURAL or key in ["weeks", "quarters", "weekday"]: relative_kwargs[key] = value else: raise AttributeError() # core datetime does not support quarters, translate to months. - relative_kwargs.setdefault('months', 0) - relative_kwargs['months'] += relative_kwargs.pop('quarters', 0) * self._MONTHS_PER_QUARTER + relative_kwargs.setdefault("months", 0) + relative_kwargs["months"] += ( + relative_kwargs.pop("quarters", 0) * self._MONTHS_PER_QUARTER + ) current = self._datetime + relativedelta(**relative_kwargs) return self.fromdatetime(current) def to(self, tz): - ''' Returns a new :class:`Arrow ` object, converted + """ Returns a new :class:`Arrow ` object, converted to the target timezone. :param tz: A :ref:`timezone expression `. @@ -639,18 +692,26 @@ def to(self, tz): >>> utc.to('local').to('utc') - ''' + """ if not isinstance(tz, tzinfo): tz = parser.TzinfoParser.parse(tz) dt = self._datetime.astimezone(tz) - return self.__class__(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, - dt.microsecond, dt.tzinfo) + return self.__class__( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + dt.tzinfo, + ) def span(self, frame, count=1): - ''' Returns two new :class:`Arrow ` objects, representing the timespan + """ Returns two new :class:`Arrow ` objects, representing the timespan of the :class:`Arrow ` object in a given timeframe. :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). @@ -672,39 +733,42 @@ def span(self, frame, count=1): >>> arrow.utcnow().span('day', count=2) (, ) - ''' + """ frame_absolute, frame_relative, relative_steps = self._get_frames(frame) - if frame_absolute == 'week': - attr = 'day' - elif frame_absolute == 'quarter': - attr = 'month' + if frame_absolute == "week": + attr = "day" + elif frame_absolute == "quarter": + attr = "month" else: attr = frame_absolute index = self._ATTRS.index(attr) - frames = self._ATTRS[:index + 1] + frames = self._ATTRS[: index + 1] values = [getattr(self, f) for f in frames] - for i in range(3 - len(values)): + for _ in range(3 - len(values)): values.append(1) floor = self.__class__(*values, tzinfo=self.tzinfo) - if frame_absolute == 'week': + if frame_absolute == "week": floor = floor + relativedelta(days=-(self.isoweekday() - 1)) - elif frame_absolute == 'quarter': + elif frame_absolute == "quarter": floor = floor + relativedelta(months=-((self.month - 1) % 3)) - ceil = floor + relativedelta( - **{frame_relative: count * relative_steps}) + relativedelta(microseconds=-1) + ceil = ( + floor + + relativedelta(**{frame_relative: count * relative_steps}) + + relativedelta(microseconds=-1) + ) return floor, ceil def floor(self, frame): - ''' Returns a new :class:`Arrow ` object, representing the "floor" + """ 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 `. @@ -715,12 +779,12 @@ def floor(self, frame): >>> arrow.utcnow().floor('hour') - ''' + """ return self.span(frame)[0] def ceil(self, frame): - ''' Returns a new :class:`Arrow ` object, representing the "ceiling" + """ 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 `. @@ -731,15 +795,14 @@ def ceil(self, frame): >>> arrow.utcnow().ceil('hour') - ''' + """ return self.span(frame)[1] - # string output and formatting. - def format(self, fmt='YYYY-MM-DD HH:mm:ssZZ', locale='en_us'): - ''' Returns a string representation of the :class:`Arrow ` object, + def format(self, fmt="YYYY-MM-DD HH:mm:ssZZ", locale="en_us"): + """ Returns a string representation of the :class:`Arrow ` object, formatted according to a format string. :param fmt: the format string. @@ -758,13 +821,14 @@ def format(self, fmt='YYYY-MM-DD HH:mm:ssZZ', locale='en_us'): >>> arrow.utcnow().format() '2013-05-09 03:56:47 -00:00' - ''' + """ return formatter.DateTimeFormatter(locale).format(self._datetime, fmt) - - def humanize(self, other=None, locale='en_us', only_distance=False, granularity='auto'): - ''' Returns a localized, humanized representation of a relative difference in time. + def humanize( + self, other=None, locale="en_us", only_distance=False, granularity="auto" + ): + """ Returns a localized, humanized representation of a relative difference in time. :param other: (optional) an :class:`Arrow ` or ``datetime`` object. Defaults to now in the current :class:`Arrow ` object's timezone. @@ -782,7 +846,7 @@ def humanize(self, other=None, locale='en_us', only_distance=False, granularity= >>> later.humanize(earlier) 'in 4 hours' - ''' + """ locale = locales.get_locale(locale) @@ -807,75 +871,77 @@ def humanize(self, other=None, locale='en_us', only_distance=False, granularity= diff = abs(delta) delta = diff - if granularity=='auto': + if granularity == "auto": if diff < 10: - return locale.describe('now', only_distance=only_distance) + return locale.describe("now", only_distance=only_distance) if diff < 45: seconds = sign * delta - return locale.describe('seconds', seconds, only_distance=only_distance) + return locale.describe("seconds", seconds, only_distance=only_distance) elif diff < 90: - return locale.describe('minute', sign, only_distance=only_distance) + return locale.describe("minute", sign, only_distance=only_distance) elif diff < 2700: minutes = sign * int(max(delta / 60, 2)) - return locale.describe('minutes', minutes, only_distance=only_distance) + return locale.describe("minutes", minutes, only_distance=only_distance) elif diff < 5400: - return locale.describe('hour', sign, only_distance=only_distance) + return locale.describe("hour", sign, only_distance=only_distance) elif diff < 79200: hours = sign * int(max(delta / 3600, 2)) - return locale.describe('hours', hours, only_distance=only_distance) + return locale.describe("hours", hours, only_distance=only_distance) elif diff < 129600: - return locale.describe('day', sign, only_distance=only_distance) + return locale.describe("day", sign, only_distance=only_distance) elif diff < 2160000: days = sign * int(max(delta / 86400, 2)) - return locale.describe('days', days, only_distance=only_distance) + return locale.describe("days", days, only_distance=only_distance) elif diff < 3888000: - return locale.describe('month', sign, only_distance=only_distance) + return locale.describe("month", sign, only_distance=only_distance) elif diff < 29808000: self_months = self._datetime.year * 12 + self._datetime.month other_months = dt.year * 12 + dt.month months = sign * int(max(abs(other_months - self_months), 2)) - return locale.describe('months', months, only_distance=only_distance) + return locale.describe("months", months, only_distance=only_distance) elif diff < 47260800: - return locale.describe('year', sign, only_distance=only_distance) + return locale.describe("year", sign, only_distance=only_distance) else: years = sign * int(max(delta / 31536000, 2)) - return locale.describe('years', years, only_distance=only_distance) + return locale.describe("years", years, only_distance=only_distance) else: - if granularity == 'second': + if granularity == "second": delta = sign * delta - if(abs(delta) < 2): - return locale.describe('now', only_distance=only_distance) - elif granularity == 'minute': + if abs(delta) < 2: + return locale.describe("now", only_distance=only_distance) + elif granularity == "minute": delta = sign * delta / float(60) - elif granularity == 'hour': - delta = sign * delta / float(60*60) - elif granularity == 'day': - delta = sign * delta / float(60*60*24) - elif granularity == 'month': - delta = sign * delta / float(60*60*24*30.5) - elif granularity == 'year': - delta = sign * delta / float(60*60*24*365.25) + elif granularity == "hour": + delta = sign * delta / float(60 * 60) + elif granularity == "day": + delta = sign * delta / float(60 * 60 * 24) + elif granularity == "month": + delta = sign * delta / float(60 * 60 * 24 * 30.5) + elif granularity == "year": + delta = sign * delta / float(60 * 60 * 24 * 365.25) else: - raise AttributeError('Error. Could not understand your level of granularity. Please select between \ - "second", "minute", "hour", "day", "week", "month" or "year"') + raise AttributeError( + 'Error. Could not understand your level of granularity. Please select between \ + "second", "minute", "hour", "day", "week", "month" or "year"' + ) - if(trunc(abs(delta)) != 1): - granularity += 's' + if trunc(abs(delta)) != 1: + granularity += "s" return locale.describe(granularity, delta, only_distance=only_distance) # query functions - def is_between(self, start, end, bounds='()'): - ''' Returns a boolean denoting whether the specified date and time is between + def is_between(self, start, end, bounds="()"): + """ Returns a boolean denoting whether the specified date and time is between the start and end dates and times. :param start: an :class:`Arrow ` object. @@ -902,33 +968,48 @@ def is_between(self, start, end, bounds='()'): >>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[)') False - ''' + """ - if bounds != '()' and bounds != '(]' and bounds != '[)' and bounds != '[]': - raise AttributeError('Error. Could not understand the specified bounds. Please select between \ - "()", "(]", "[)", or "[]"') + if bounds != "()" and bounds != "(]" and bounds != "[)" and bounds != "[]": + raise AttributeError( + 'Error. Could not understand the specified bounds. Please select between \ + "()", "(]", "[)", or "[]"' + ) if not isinstance(start, Arrow): - raise TypeError('Can\'t parse start date argument type of \'{}\''.format(type(start))) + raise TypeError( + "Can't parse start date argument type of '{}'".format(type(start)) + ) if not isinstance(end, Arrow): - raise TypeError('Can\'t parse end date argument type of \'{}\''.format(type(end))) + raise TypeError( + "Can't parse end date argument type of '{}'".format(type(end)) + ) - include_start = (bounds[0] == '[') - include_end = (bounds[1] == ']') + include_start = bounds[0] == "[" + include_end = bounds[1] == "]" target_timestamp = self.float_timestamp start_timestamp = start.float_timestamp end_timestamp = end.float_timestamp if include_start and include_end: - return target_timestamp >= start_timestamp and target_timestamp <= end_timestamp + return ( + target_timestamp >= start_timestamp + and target_timestamp <= end_timestamp + ) elif include_start and not include_end: - return target_timestamp >= start_timestamp and target_timestamp < end_timestamp + return ( + target_timestamp >= start_timestamp and target_timestamp < end_timestamp + ) elif not include_start and include_end: - return target_timestamp > start_timestamp and target_timestamp <= end_timestamp + return ( + target_timestamp > start_timestamp and target_timestamp <= end_timestamp + ) else: - return target_timestamp > start_timestamp and target_timestamp < end_timestamp + return ( + target_timestamp > start_timestamp and target_timestamp < end_timestamp + ) # math @@ -962,7 +1043,6 @@ def __rsub__(self, other): return NotImplemented - # comparisons def __eq__(self, other): @@ -1008,41 +1088,40 @@ def __le__(self, other): return self._datetime <= self._get_datetime(other) def __cmp__(self, other): - if sys.version_info[0] < 3: # pragma: no cover + if sys.version_info[0] < 3: # pragma: no cover if not isinstance(other, (Arrow, datetime)): - raise TypeError('can\'t compare \'{}\' to \'{}\''.format( - type(self), type(other))) - - + raise TypeError( + "can't compare '{}' to '{}'".format(type(self), type(other)) + ) # datetime methods def date(self): - ''' Returns a ``date`` object with the same year, month and day. + """ Returns a ``date`` object with the same year, month and day. Usage:: >>> arrow.utcnow().date() datetime.date(2019, 1, 23) - ''' + """ return self._datetime.date() def time(self): - ''' Returns a ``time`` object with the same hour, minute, second, microsecond. + """ Returns a ``time`` object with the same hour, minute, second, microsecond. Usage:: >>> arrow.utcnow().time() datetime.time(12, 15, 34, 68352) - ''' + """ return self._datetime.time() def timetz(self): - ''' Returns a ``time`` object with the same hour, minute, second, microsecond and + """ Returns a ``time`` object with the same hour, minute, second, microsecond and tzinfo. Usage:: @@ -1050,12 +1129,12 @@ def timetz(self): >>> arrow.utcnow().timetz() datetime.time(12, 5, 18, 298893, tzinfo=tzutc()) - ''' + """ return self._datetime.timetz() def astimezone(self, tz): - ''' Returns a ``datetime`` object, converted to the specified timezone. + """ Returns a ``datetime`` object, converted to the specified timezone. :param tz: a ``tzinfo`` object. @@ -1066,12 +1145,12 @@ def astimezone(self, tz): >>> pacific.astimezone(nyc) datetime.datetime(2019, 1, 20, 10, 24, 22, 328172, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York')) - ''' + """ return self._datetime.astimezone(tz) def utcoffset(self): - ''' Returns a ``timedelta`` object representing the whole number of minutes difference from + """ Returns a ``timedelta`` object representing the whole number of minutes difference from UTC time. Usage:: @@ -1079,120 +1158,120 @@ def utcoffset(self): >>> arrow.now('US/Pacific').utcoffset() datetime.timedelta(-1, 57600) - ''' + """ return self._datetime.utcoffset() def dst(self): - ''' Returns the daylight savings time adjustment. + """ Returns the daylight savings time adjustment. Usage:: >>> arrow.utcnow().dst() datetime.timedelta(0) - ''' + """ return self._datetime.dst() def timetuple(self): - ''' Returns a ``time.struct_time``, in the current timezone. + """ Returns a ``time.struct_time``, in the current timezone. Usage:: >>> arrow.utcnow().timetuple() time.struct_time(tm_year=2019, tm_mon=1, tm_mday=20, tm_hour=15, tm_min=17, tm_sec=8, tm_wday=6, tm_yday=20, tm_isdst=0) - ''' + """ return self._datetime.timetuple() def utctimetuple(self): - ''' Returns a ``time.struct_time``, in UTC time. + """ Returns a ``time.struct_time``, in UTC time. Usage:: >>> arrow.utcnow().utctimetuple() time.struct_time(tm_year=2019, tm_mon=1, tm_mday=19, tm_hour=21, tm_min=41, tm_sec=7, tm_wday=5, tm_yday=19, tm_isdst=0) - ''' + """ return self._datetime.utctimetuple() def toordinal(self): - ''' Returns the proleptic Gregorian ordinal of the date. + """ Returns the proleptic Gregorian ordinal of the date. Usage:: >>> arrow.utcnow().toordinal() 737078 - ''' + """ return self._datetime.toordinal() def weekday(self): - ''' Returns the day of the week as an integer (0-6). + """ Returns the day of the week as an integer (0-6). Usage:: >>> arrow.utcnow().weekday() 5 - ''' + """ return self._datetime.weekday() def isoweekday(self): - ''' Returns the ISO day of the week as an integer (1-7). + """ Returns the ISO day of the week as an integer (1-7). Usage:: >>> arrow.utcnow().isoweekday() 6 - ''' + """ return self._datetime.isoweekday() def isocalendar(self): - ''' Returns a 3-tuple, (ISO year, ISO week number, ISO weekday). + """ Returns a 3-tuple, (ISO year, ISO week number, ISO weekday). Usage:: >>> arrow.utcnow().isocalendar() (2019, 3, 6) - ''' + """ return self._datetime.isocalendar() - def isoformat(self, sep='T'): - '''Returns an ISO 8601 formatted representation of the date and time. + def isoformat(self, sep="T"): + """Returns an ISO 8601 formatted representation of the date and time. Usage:: >>> arrow.utcnow().isoformat() '2019-01-19T18:30:52.442118+00:00' - ''' + """ return self._datetime.isoformat(sep) def ctime(self): - ''' Returns a ctime formatted representation of the date and time. + """ Returns a ctime formatted representation of the date and time. Usage:: >>> arrow.utcnow().ctime() 'Sat Jan 19 18:26:50 2019' - ''' + """ return self._datetime.ctime() def strftime(self, format): - ''' Formats in the style of ``datetime.strftime``. + """ Formats in the style of ``datetime.strftime``. :param format: the format string. @@ -1201,19 +1280,19 @@ def strftime(self, format): >>> arrow.utcnow().strftime('%d-%m-%Y %H:%M:%S') '23-01-2019 12:28:17' - ''' + """ return self._datetime.strftime(format) def for_json(self): - '''Serializes for the ``for_json`` protocol of simplejson. + """Serializes for the ``for_json`` protocol of simplejson. Usage:: >>> arrow.utcnow().for_json() '2019-01-19T18:25:36.760079+00:00' - ''' + """ return self.isoformat() @@ -1230,8 +1309,7 @@ def _get_tzinfo(tz_expr): try: return parser.TzinfoParser.parse(tz_expr) except parser.ParserError: - raise ValueError('\'{}\' not recognized as a timezone'.format( - tz_expr)) + raise ValueError("'{}' not recognized as a timezone".format(tz_expr)) @classmethod def _get_datetime(cls, expr): @@ -1245,23 +1323,28 @@ def _get_datetime(cls, expr): try: expr = float(expr) return cls.utcfromtimestamp(expr).datetime - except: + except Exception: raise ValueError( - '\'{}\' not recognized as a timestamp or datetime'.format(expr)) + "'{}' not recognized as a timestamp or datetime".format(expr) + ) @classmethod def _get_frames(cls, name): if name in cls._ATTRS: - return name, '{}s'.format(name), 1 + return name, "{}s".format(name), 1 - elif name in ['week', 'weeks']: - return 'week', 'weeks', 1 - elif name in ['quarter', 'quarters']: - return 'quarter', 'months', 3 + elif name in ["week", "weeks"]: + return "week", "weeks", 1 + elif name in ["quarter", "quarters"]: + return "quarter", "months", 3 - supported = ', '.join(cls._ATTRS + ['week', 'weeks'] + ['quarter', 'quarters']) - raise AttributeError('range/span over frame {} not supported. Supported frames: {}'.format(name, supported)) + supported = ", ".join(cls._ATTRS + ["week", "weeks"] + ["quarter", "quarters"]) + raise AttributeError( + "range/span over frame {} not supported. Supported frames: {}".format( + name, supported + ) + ) @classmethod def _get_iteration_params(cls, end, limit): @@ -1269,7 +1352,7 @@ def _get_iteration_params(cls, end, limit): if end is None: if limit is None: - raise Exception('one of \'end\' or \'limit\' is required') + raise Exception("one of 'end' or 'limit' is required") return cls.max, limit @@ -1283,8 +1366,9 @@ def _get_timestamp_from_input(timestamp): try: return float(timestamp) - except: - raise ValueError('cannot parse \'{}\' as a timestamp'.format(timestamp)) + except Exception: + raise ValueError("cannot parse '{}' as a timestamp".format(timestamp)) + Arrow.min = Arrow.fromdatetime(datetime.min) Arrow.max = Arrow.fromdatetime(datetime.max) diff --git a/arrow/factory.py b/arrow/factory.py index 535dbc57c..8591443a4 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -8,29 +8,30 @@ from __future__ import absolute_import -from arrow.arrow import Arrow -from arrow import parser -from arrow.util import is_timestamp, isstr +import calendar +from datetime import date, datetime, tzinfo +from time import struct_time -from datetime import datetime, tzinfo, date from dateutil import tz as dateutil_tz -from time import struct_time -import calendar + +from arrow import parser +from arrow.arrow import Arrow +from arrow.util import is_timestamp, isstr class ArrowFactory(object): - ''' A factory for generating :class:`Arrow ` objects. + """ A factory for generating :class:`Arrow ` objects. :param type: (optional) the :class:`Arrow `-based class to construct from. Defaults to :class:`Arrow `. - ''' + """ def __init__(self, type=Arrow): self.type = type def get(self, *args, **kwargs): - ''' Returns an :class:`Arrow ` object based on flexible inputs. + """ Returns an :class:`Arrow ` object based on flexible inputs. :param locale: (optional) a ``str`` specifying a locale for the parser. Defaults to 'en_us'. @@ -130,11 +131,11 @@ def get(self, *args, **kwargs): >>> arrow.get(gmtime(0)) - ''' + """ arg_count = len(args) - locale = kwargs.get('locale', 'en_us') - tz = kwargs.get('tzinfo', None) + locale = kwargs.get("locale", "en_us") + tz = kwargs.get("tzinfo", None) # () -> now, @ utc. if arg_count == 0: @@ -179,7 +180,9 @@ def get(self, *args, **kwargs): return self.type.utcfromtimestamp(calendar.timegm(arg)) else: - raise TypeError('Can\'t parse single argument type of \'{}\''.format(type(arg))) + raise TypeError( + "Can't parse single argument type of '{}'".format(type(arg)) + ) elif arg_count == 2: @@ -191,8 +194,11 @@ def get(self, *args, **kwargs): if isinstance(arg_2, tzinfo) or isstr(arg_2): return self.type.fromdatetime(arg_1, arg_2) else: - raise TypeError('Can\'t parse two arguments of types \'datetime\', \'{}\''.format( - type(arg_2))) + raise TypeError( + "Can't parse two arguments of types 'datetime', '{}'".format( + type(arg_2) + ) + ) elif isinstance(arg_1, date): @@ -200,8 +206,11 @@ def get(self, *args, **kwargs): if isinstance(arg_2, tzinfo) or isstr(arg_2): return self.type.fromdate(arg_1, tzinfo=arg_2) else: - raise TypeError('Can\'t parse two arguments of types \'date\', \'{}\''.format( - type(arg_2))) + raise TypeError( + "Can't parse two arguments of types 'date', '{}'".format( + type(arg_2) + ) + ) # (str, format) -> parse. elif isstr(arg_1) and (isstr(arg_2) or isinstance(arg_2, list)): @@ -209,27 +218,30 @@ def get(self, *args, **kwargs): return self.type.fromdatetime(dt, tzinfo=tz) else: - raise TypeError('Can\'t parse two arguments of types \'{}\', \'{}\''.format( - type(arg_1), type(arg_2))) + raise TypeError( + "Can't parse two arguments of types '{}', '{}'".format( + type(arg_1), type(arg_2) + ) + ) # 3+ args -> datetime-like via constructor. else: return self.type(*args, **kwargs) def utcnow(self): - '''Returns an :class:`Arrow ` object, representing "now" in UTC time. + """Returns an :class:`Arrow ` object, representing "now" in UTC time. Usage:: >>> import arrow >>> arrow.utcnow() - ''' + """ return self.type.utcnow() def now(self, tz=None): - '''Returns an :class:`Arrow ` object, representing "now" in the given + """Returns an :class:`Arrow ` object, representing "now" in the given timezone. :param tz: (optional) A :ref:`timezone expression `. Defaults to local time. @@ -248,7 +260,7 @@ def now(self, tz=None): >>> arrow.now('local') - ''' + """ if tz is None: tz = dateutil_tz.tzlocal() diff --git a/arrow/formatter.py b/arrow/formatter.py index 5194c356b..08c89e04d 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -3,15 +3,19 @@ import calendar import re + from dateutil import tz as dateutil_tz -from arrow import util, locales + +from arrow import locales, util class DateTimeFormatter(object): - _FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X)') + _FORMAT_RE = re.compile( + r"(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X)" + ) - def __init__(self, locale='en_us'): + def __init__(self, locale="en_us"): self.locale = locales.get_locale(locale) @@ -21,88 +25,87 @@ def format(cls, dt, fmt): def _format_token(self, dt, token): - if token == 'YYYY': + if token == "YYYY": return self.locale.year_full(dt.year) - if token == 'YY': + if token == "YY": return self.locale.year_abbreviation(dt.year) - if token == 'MMMM': + if token == "MMMM": return self.locale.month_name(dt.month) - if token == 'MMM': + if token == "MMM": return self.locale.month_abbreviation(dt.month) - if token == 'MM': - return '{:02d}'.format(dt.month) - if token == 'M': + if token == "MM": + return "{:02d}".format(dt.month) + if token == "M": return str(dt.month) - if token == 'DDDD': - return '{:03d}'.format(dt.timetuple().tm_yday) - if token == 'DDD': + if token == "DDDD": + return "{:03d}".format(dt.timetuple().tm_yday) + if token == "DDD": return str(dt.timetuple().tm_yday) - if token == 'DD': - return '{:02d}'.format(dt.day) - if token == 'D': + if token == "DD": + return "{:02d}".format(dt.day) + if token == "D": return str(dt.day) - if token == 'Do': + if token == "Do": return self.locale.ordinal_number(dt.day) - if token == 'dddd': + if token == "dddd": return self.locale.day_name(dt.isoweekday()) - if token == 'ddd': + if token == "ddd": return self.locale.day_abbreviation(dt.isoweekday()) - if token == 'd': + if token == "d": return str(dt.isoweekday()) - if token == 'HH': - return '{:02d}'.format(dt.hour) - if token == 'H': + if token == "HH": + return "{:02d}".format(dt.hour) + if token == "H": return str(dt.hour) - if token == 'hh': - return '{:02d}'.format(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) - if token == 'h': + if token == "hh": + return "{:02d}".format(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) + if token == "h": return str(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) - if token == 'mm': - return '{:02d}'.format(dt.minute) - if token == 'm': + if token == "mm": + return "{:02d}".format(dt.minute) + if token == "m": return str(dt.minute) - if token == 'ss': - return '{:02d}'.format(dt.second) - if token == 's': + if token == "ss": + return "{:02d}".format(dt.second) + if token == "s": return str(dt.second) - if token == 'SSSSSS': - return str('{:06d}'.format(int(dt.microsecond))) - if token == 'SSSSS': - return str('{:05d}'.format(int(dt.microsecond / 10))) - if token == 'SSSS': - return str('{:04d}'.format(int(dt.microsecond / 100))) - if token == 'SSS': - return str('{:03d}'.format(int(dt.microsecond / 1000))) - if token == 'SS': - return str('{:02d}'.format(int(dt.microsecond / 10000))) - if token == 'S': + if token == "SSSSSS": + return str("{:06d}".format(int(dt.microsecond))) + if token == "SSSSS": + return str("{:05d}".format(int(dt.microsecond / 10))) + if token == "SSSS": + return str("{:04d}".format(int(dt.microsecond / 100))) + if token == "SSS": + return str("{:03d}".format(int(dt.microsecond / 1000))) + if token == "SS": + return str("{:02d}".format(int(dt.microsecond / 10000))) + if token == "S": return str(int(dt.microsecond / 100000)) - if token == 'X': + if token == "X": return str(calendar.timegm(dt.utctimetuple())) - if token == 'ZZZ': + if token == "ZZZ": return dt.tzname() - if token in ['ZZ', 'Z']: - separator = ':' if token == 'ZZ' else '' + if token in ["ZZ", "Z"]: + separator = ":" if token == "ZZ" else "" tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo total_minutes = int(util.total_seconds(tz.utcoffset(dt)) / 60) - sign = '+' if total_minutes >= 0 else '-' + sign = "+" if total_minutes >= 0 else "-" total_minutes = abs(total_minutes) hour, minute = divmod(total_minutes, 60) - return '{}{:02d}{}{:02d}'.format(sign, hour, separator, minute) + return "{}{:02d}{}{:02d}".format(sign, hour, separator, minute) - if token in ('a', 'A'): + if token in ("a", "A"): return self.locale.meridian(dt.hour, token) - diff --git a/arrow/locales.py b/arrow/locales.py index db9433a09..5a09e5b68 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals import inspect import sys @@ -8,49 +7,45 @@ def get_locale(name): - '''Returns an appropriate :class:`Locale ` + """Returns an appropriate :class:`Locale ` corresponding to an inpute locale name. :param name: the name of the locale. - ''' + """ locale_cls = _locales.get(name.lower()) if locale_cls is None: - raise ValueError('Unsupported locale \'{}\''.format(name)) + raise ValueError("Unsupported locale '{}'".format(name)) return locale_cls() # base locale type. + class Locale(object): - ''' Represents locale-specific data and functionality. ''' + """ Represents locale-specific data and functionality. """ names = [] timeframes = { - 'now': '', - 'seconds': '', - 'minute': '', - 'minutes': '', - 'hour': '', - 'hours': '', - 'day': '', - 'days': '', - 'month': '', - 'months': '', - 'year': '', - 'years': '', - } - - meridians = { - 'am': '', - 'pm': '', - 'AM': '', - 'PM': '', - } + "now": "", + "seconds": "", + "minute": "", + "minutes": "", + "hour": "", + "hours": "", + "day": "", + "days": "", + "month": "", + "months": "", + "year": "", + "years": "", + } + + meridians = {"am": "", "pm": "", "AM": "", "PM": ""} past = None future = None @@ -61,19 +56,19 @@ class Locale(object): day_names = [] day_abbreviations = [] - ordinal_day_re = r'(\d+)' + ordinal_day_re = r"(\d+)" def __init__(self): self._month_name_to_ordinal = None def describe(self, timeframe, delta=0, only_distance=False): - ''' Describes a delta within a timeframe in plain language. + """ Describes a delta within a timeframe in plain language. :param timeframe: a string representing a timeframe. :param delta: a quantity representing a delta in a timeframe. :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords - ''' + """ humanized = self._format_timeframe(timeframe, delta) if not only_distance: @@ -82,89 +77,91 @@ def describe(self, timeframe, delta=0, only_distance=False): return humanized def day_name(self, day): - ''' Returns the day name for a specified day of the week. + """ Returns the day name for a specified day of the week. :param day: the ``int`` day of the week (1-7). - ''' + """ return self.day_names[day] def day_abbreviation(self, day): - ''' Returns the day abbreviation for a specified day of the week. + """ Returns the day abbreviation for a specified day of the week. :param day: the ``int`` day of the week (1-7). - ''' + """ return self.day_abbreviations[day] def month_name(self, month): - ''' Returns the month name for a specified month of the year. + """ Returns the month name for a specified month of the year. :param month: the ``int`` month of the year (1-12). - ''' + """ return self.month_names[month] def month_abbreviation(self, month): - ''' Returns the month abbreviation for a specified month of the year. + """ Returns the month abbreviation for a specified month of the year. :param month: the ``int`` month of the year (1-12). - ''' + """ return self.month_abbreviations[month] def month_number(self, name): - ''' Returns the month number for a month specified by name or abbreviation. + """ Returns the month number for a month specified by name or abbreviation. :param name: the month name or abbreviation. - ''' + """ if self._month_name_to_ordinal is None: self._month_name_to_ordinal = self._name_to_ordinal(self.month_names) - self._month_name_to_ordinal.update(self._name_to_ordinal(self.month_abbreviations)) + self._month_name_to_ordinal.update( + self._name_to_ordinal(self.month_abbreviations) + ) return self._month_name_to_ordinal.get(name) def year_full(self, year): - ''' Returns the year for specific locale if available + """ Returns the year for specific locale if available :param name: the ``int`` year (4-digit) - ''' - return '{:04d}'.format(year) + """ + return "{:04d}".format(year) def year_abbreviation(self, year): - ''' Returns the year for specific locale if available + """ Returns the year for specific locale if available :param name: the ``int`` year (4-digit) - ''' - return '{:04d}'.format(year)[2:] + """ + return "{:04d}".format(year)[2:] def meridian(self, hour, token): - ''' Returns the meridian indicator for a specified hour and format token. + """ Returns the meridian indicator for a specified hour and format token. :param hour: the ``int`` hour of the day. :param token: the format token. - ''' + """ - if token == 'a': - return self.meridians['am'] if hour < 12 else self.meridians['pm'] - if token == 'A': - return self.meridians['AM'] if hour < 12 else self.meridians['PM'] + if token == "a": + return self.meridians["am"] if hour < 12 else self.meridians["pm"] + if token == "A": + return self.meridians["AM"] if hour < 12 else self.meridians["PM"] def ordinal_number(self, n): - ''' Returns the ordinal format of a given integer + """ Returns the ordinal format of a given integer :param n: an integer - ''' + """ return self._ordinal_number(n) def _ordinal_number(self, n): - return '{}'.format(n) + return "{}".format(n) def _name_to_ordinal(self, lst): return dict(map(lambda i: (i[1].lower(), i[0] + 1), enumerate(lst[1:]))) @@ -174,7 +171,7 @@ def _format_timeframe(self, timeframe, delta): def _format_relative(self, humanized, timeframe, delta): - if timeframe == 'now': + if timeframe == "now": return humanized direction = self.past if delta < 0 else self.future @@ -184,312 +181,591 @@ def _format_relative(self, humanized, timeframe, delta): # base locale type implementations. + class EnglishLocale(Locale): - names = ['en', 'en_us', 'en_gb', 'en_au', 'en_be', 'en_jp', 'en_za', 'en_ca', 'en_ph'] + names = [ + "en", + "en_us", + "en_gb", + "en_au", + "en_be", + "en_jp", + "en_za", + "en_ca", + "en_ph", + ] - past = '{0} ago' - future = 'in {0}' + past = "{0} ago" + future = "in {0}" timeframes = { - 'now': 'just now', - 'seconds': 'seconds', - 'minute': 'a minute', - 'minutes': '{0} minutes', - 'hour': 'an hour', - 'hours': '{0} hours', - 'day': 'a day', - 'days': '{0} days', - 'month': 'a month', - 'months': '{0} months', - 'year': 'a year', - 'years': '{0} years', - } - - meridians = { - 'am': 'am', - 'pm': 'pm', - 'AM': 'AM', - 'PM': 'PM', - } + "now": "just now", + "seconds": "seconds", + "minute": "a minute", + "minutes": "{0} minutes", + "hour": "an hour", + "hours": "{0} hours", + "day": "a day", + "days": "{0} days", + "month": "a month", + "months": "{0} months", + "year": "a year", + "years": "{0} years", + } + + meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"} - 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'] + 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 = ['', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] - day_abbreviations = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + day_names = [ + "", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ] + day_abbreviations = ["", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - ordinal_day_re = r'((?P[2-3]?1(?=st)|[2-3]?2(?=nd)|[2-3]?3(?=rd)|[1-3]?[04-9](?=th)|1[1-3](?=th))(st|nd|rd|th))' + ordinal_day_re = r"((?P[2-3]?1(?=st)|[2-3]?2(?=nd)|[2-3]?3(?=rd)|[1-3]?[04-9](?=th)|1[1-3](?=th))(st|nd|rd|th))" def _ordinal_number(self, n): if n % 100 not in (11, 12, 13): remainder = abs(n) % 10 if remainder == 1: - return '{}st'.format(n) + return "{}st".format(n) elif remainder == 2: - return '{}nd'.format(n) + return "{}nd".format(n) elif remainder == 3: - return '{}rd'.format(n) - return '{}th'.format(n) + return "{}rd".format(n) + return "{}th".format(n) def describe(self, timeframe, delta=0, only_distance=False): - ''' Describes a delta within a timeframe in plain language. + """ Describes a delta within a timeframe in plain language. :param timeframe: a string representing a timeframe. :param delta: a quantity representing a delta in a timeframe. :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords - ''' + """ humanized = super(EnglishLocale, self).describe(timeframe, delta, only_distance) - if only_distance and timeframe == 'now': - humanized = 'instantly' + if only_distance and timeframe == "now": + humanized = "instantly" return humanized + class ItalianLocale(Locale): - names = ['it', 'it_it'] - past = '{0} fa' - future = 'tra {0}' + names = ["it", "it_it"] + past = "{0} fa" + future = "tra {0}" timeframes = { - 'now': 'adesso', - 'seconds': 'qualche secondo', - 'minute': 'un minuto', - 'minutes': '{0} minuti', - 'hour': 'un\'ora', - 'hours': '{0} ore', - 'day': 'un giorno', - 'days': '{0} giorni', - 'month': 'un mese', - 'months': '{0} mesi', - 'year': 'un anno', - 'years': '{0} anni', + "now": "adesso", + "seconds": "qualche secondo", + "minute": "un minuto", + "minutes": "{0} minuti", + "hour": "un'ora", + "hours": "{0} ore", + "day": "un giorno", + "days": "{0} giorni", + "month": "un mese", + "months": "{0} mesi", + "year": "un anno", + "years": "{0} anni", } - month_names = ['', 'gennaio', 'febbraio', 'marzo', 'aprile', 'maggio', 'giugno', 'luglio', - 'agosto', 'settembre', 'ottobre', 'novembre', 'dicembre'] - month_abbreviations = ['', 'gen', 'feb', 'mar', 'apr', 'mag', 'giu', 'lug', 'ago', - 'set', 'ott', 'nov', 'dic'] + month_names = [ + "", + "gennaio", + "febbraio", + "marzo", + "aprile", + "maggio", + "giugno", + "luglio", + "agosto", + "settembre", + "ottobre", + "novembre", + "dicembre", + ] + month_abbreviations = [ + "", + "gen", + "feb", + "mar", + "apr", + "mag", + "giu", + "lug", + "ago", + "set", + "ott", + "nov", + "dic", + ] - day_names = ['', 'lunedì', 'martedì', 'mercoledì', 'giovedì', 'venerdì', 'sabato', 'domenica'] - day_abbreviations = ['', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab', 'dom'] + day_names = [ + "", + "lunedì", + "martedì", + "mercoledì", + "giovedì", + "venerdì", + "sabato", + "domenica", + ] + day_abbreviations = ["", "lun", "mar", "mer", "gio", "ven", "sab", "dom"] - ordinal_day_re = r'((?P[1-3]?[0-9](?=[ºª]))[ºª])' + ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])" def _ordinal_number(self, n): - return '{}º'.format(n) + return "{}º".format(n) class SpanishLocale(Locale): - names = ['es', 'es_es'] - past = 'hace {0}' - future = 'en {0}' + names = ["es", "es_es"] + past = "hace {0}" + future = "en {0}" timeframes = { - 'now': 'ahora', - 'seconds': 'segundos', - 'minute': 'un minuto', - 'minutes': '{0} minutos', - 'hour': 'una hora', - 'hours': '{0} horas', - 'day': 'un día', - 'days': '{0} días', - 'month': 'un mes', - 'months': '{0} meses', - 'year': 'un año', - 'years': '{0} años', + "now": "ahora", + "seconds": "segundos", + "minute": "un minuto", + "minutes": "{0} minutos", + "hour": "una hora", + "hours": "{0} horas", + "day": "un día", + "days": "{0} días", + "month": "un mes", + "months": "{0} meses", + "year": "un año", + "years": "{0} años", } - month_names = ['', 'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', - 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'] - month_abbreviations = ['', 'ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', - 'sep', 'oct', 'nov', 'dic'] + month_names = [ + "", + "enero", + "febrero", + "marzo", + "abril", + "mayo", + "junio", + "julio", + "agosto", + "septiembre", + "octubre", + "noviembre", + "diciembre", + ] + month_abbreviations = [ + "", + "ene", + "feb", + "mar", + "abr", + "may", + "jun", + "jul", + "ago", + "sep", + "oct", + "nov", + "dic", + ] - day_names = ['', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado', 'domingo'] - day_abbreviations = ['', 'lun', 'mar', 'mie', 'jue', 'vie', 'sab', 'dom'] + day_names = [ + "", + "lunes", + "martes", + "miércoles", + "jueves", + "viernes", + "sábado", + "domingo", + ] + day_abbreviations = ["", "lun", "mar", "mie", "jue", "vie", "sab", "dom"] - ordinal_day_re = r'((?P[1-3]?[0-9](?=[ºª]))[ºª])' + ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])" def _ordinal_number(self, n): - return '{}º'.format(n) + return "{}º".format(n) class FrenchLocale(Locale): - names = ['fr', 'fr_fr'] - past = 'il y a {0}' - future = 'dans {0}' + names = ["fr", "fr_fr"] + past = "il y a {0}" + future = "dans {0}" timeframes = { - 'now': 'maintenant', - 'seconds': 'quelques secondes', - 'minute': 'une minute', - 'minutes': '{0} minutes', - 'hour': 'une heure', - 'hours': '{0} heures', - 'day': 'un jour', - 'days': '{0} jours', - 'month': 'un mois', - 'months': '{0} mois', - 'year': 'un an', - 'years': '{0} ans', + "now": "maintenant", + "seconds": "quelques secondes", + "minute": "une minute", + "minutes": "{0} minutes", + "hour": "une heure", + "hours": "{0} heures", + "day": "un jour", + "days": "{0} jours", + "month": "un mois", + "months": "{0} mois", + "year": "un an", + "years": "{0} ans", } - month_names = ['', 'janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', - 'août', 'septembre', 'octobre', 'novembre', 'décembre'] - month_abbreviations = ['', 'janv', 'févr', 'mars', 'avr', 'mai', 'juin', 'juil', 'août', - 'sept', 'oct', 'nov', 'déc'] + month_names = [ + "", + "janvier", + "février", + "mars", + "avril", + "mai", + "juin", + "juillet", + "août", + "septembre", + "octobre", + "novembre", + "décembre", + ] + month_abbreviations = [ + "", + "janv", + "févr", + "mars", + "avr", + "mai", + "juin", + "juil", + "août", + "sept", + "oct", + "nov", + "déc", + ] - day_names = ['', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche'] - day_abbreviations = ['', 'lun', 'mar', 'mer', 'jeu', 'ven', 'sam', 'dim'] + day_names = [ + "", + "lundi", + "mardi", + "mercredi", + "jeudi", + "vendredi", + "samedi", + "dimanche", + ] + day_abbreviations = ["", "lun", "mar", "mer", "jeu", "ven", "sam", "dim"] - ordinal_day_re = r'((?P\b1(?=er\b)|[1-3]?[02-9](?=e\b)|[1-3]1(?=e\b))(er|e)\b)' + ordinal_day_re = ( + r"((?P\b1(?=er\b)|[1-3]?[02-9](?=e\b)|[1-3]1(?=e\b))(er|e)\b)" + ) def _ordinal_number(self, n): if abs(n) == 1: - return '{}er'.format(n) - return '{}e'.format(n) + return "{}er".format(n) + return "{}e".format(n) class GreekLocale(Locale): - names = ['el', 'el_gr'] + names = ["el", "el_gr"] - past = '{0} πριν' - future = 'σε {0}' + past = "{0} πριν" + future = "σε {0}" timeframes = { - 'now': 'τώρα', - 'seconds': 'δευτερόλεπτα', - 'minute': 'ένα λεπτό', - 'minutes': '{0} λεπτά', - 'hour': 'μία ώρα', - 'hours': '{0} ώρες', - 'day': 'μία μέρα', - 'days': '{0} μέρες', - 'month': 'ένα μήνα', - 'months': '{0} μήνες', - 'year': 'ένα χρόνο', - 'years': '{0} χρόνια', + "now": "τώρα", + "seconds": "δευτερόλεπτα", + "minute": "ένα λεπτό", + "minutes": "{0} λεπτά", + "hour": "μία ώρα", + "hours": "{0} ώρες", + "day": "μία μέρα", + "days": "{0} μέρες", + "month": "ένα μήνα", + "months": "{0} μήνες", + "year": "ένα χρόνο", + "years": "{0} χρόνια", } - month_names = ['', 'Ιανουαρίου', 'Φεβρουαρίου', 'Μαρτίου', 'Απριλίου', 'Μαΐου', 'Ιουνίου', - 'Ιουλίου', 'Αυγούστου', 'Σεπτεμβρίου', 'Οκτωβρίου', 'Νοεμβρίου', 'Δεκεμβρίου'] - month_abbreviations = ['', 'Ιαν', 'Φεβ', 'Μαρ', 'Απρ', 'Μαϊ', 'Ιον', 'Ιολ', 'Αυγ', - 'Σεπ', 'Οκτ', 'Νοε', 'Δεκ'] + month_names = [ + "", + "Ιανουαρίου", + "Φεβρουαρίου", + "Μαρτίου", + "Απριλίου", + "Μαΐου", + "Ιουνίου", + "Ιουλίου", + "Αυγούστου", + "Σεπτεμβρίου", + "Οκτωβρίου", + "Νοεμβρίου", + "Δεκεμβρίου", + ] + month_abbreviations = [ + "", + "Ιαν", + "Φεβ", + "Μαρ", + "Απρ", + "Μαϊ", + "Ιον", + "Ιολ", + "Αυγ", + "Σεπ", + "Οκτ", + "Νοε", + "Δεκ", + ] - day_names = ['', 'Δευτέρα', 'Τρίτη', 'Τετάρτη', 'Πέμπτη', 'Παρασκευή', 'Σάββατο', 'Κυριακή'] - day_abbreviations = ['', 'Δευ', 'Τρι', 'Τετ', 'Πεμ', 'Παρ', 'Σαβ', 'Κυρ'] + day_names = [ + "", + "Δευτέρα", + "Τρίτη", + "Τετάρτη", + "Πέμπτη", + "Παρασκευή", + "Σάββατο", + "Κυριακή", + ] + day_abbreviations = ["", "Δευ", "Τρι", "Τετ", "Πεμ", "Παρ", "Σαβ", "Κυρ"] class JapaneseLocale(Locale): - names = ['ja', 'ja_jp'] + names = ["ja", "ja_jp"] - past = '{0}前' - future = '{0}後' + past = "{0}前" + future = "{0}後" timeframes = { - 'now': '現在', - 'seconds': '数秒', - 'minute': '1分', - 'minutes': '{0}分', - 'hour': '1時間', - 'hours': '{0}時間', - 'day': '1日', - 'days': '{0}日', - 'month': '1ヶ月', - 'months': '{0}ヶ月', - 'year': '1年', - 'years': '{0}年', + "now": "現在", + "seconds": "数秒", + "minute": "1分", + "minutes": "{0}分", + "hour": "1時間", + "hours": "{0}時間", + "day": "1日", + "days": "{0}日", + "month": "1ヶ月", + "months": "{0}ヶ月", + "year": "1年", + "years": "{0}年", } - month_names = ['', '1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', - '9月', '10月', '11月', '12月'] - month_abbreviations = ['', ' 1', ' 2', ' 3', ' 4', ' 5', ' 6', ' 7', ' 8', - ' 9', '10', '11', '12'] + month_names = [ + "", + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月", + ] + month_abbreviations = [ + "", + " 1", + " 2", + " 3", + " 4", + " 5", + " 6", + " 7", + " 8", + " 9", + "10", + "11", + "12", + ] - day_names = ['', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日'] - day_abbreviations = ['', '月', '火', '水', '木', '金', '土', '日'] + day_names = ["", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日", "日曜日"] + day_abbreviations = ["", "月", "火", "水", "木", "金", "土", "日"] class SwedishLocale(Locale): - names = ['sv', 'sv_se'] + names = ["sv", "sv_se"] - past = 'för {0} sen' - future = 'om {0}' + past = "för {0} sen" + future = "om {0}" timeframes = { - 'now': 'just nu', - 'seconds': 'några sekunder', - 'minute': 'en minut', - 'minutes': '{0} minuter', - 'hour': 'en timme', - 'hours': '{0} timmar', - 'day': 'en dag', - 'days': '{0} dagar', - 'month': 'en månad', - 'months': '{0} månader', - 'year': 'ett år', - 'years': '{0} år', + "now": "just nu", + "seconds": "några sekunder", + "minute": "en minut", + "minutes": "{0} minuter", + "hour": "en timme", + "hours": "{0} timmar", + "day": "en dag", + "days": "{0} dagar", + "month": "en månad", + "months": "{0} månader", + "year": "ett år", + "years": "{0} år", } - month_names = ['', 'januari', 'februari', 'mars', 'april', 'maj', 'juni', 'juli', - 'augusti', 'september', 'oktober', 'november', 'december'] - month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', - 'aug', 'sep', 'okt', 'nov', 'dec'] + month_names = [ + "", + "januari", + "februari", + "mars", + "april", + "maj", + "juni", + "juli", + "augusti", + "september", + "oktober", + "november", + "december", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mar", + "apr", + "maj", + "jun", + "jul", + "aug", + "sep", + "okt", + "nov", + "dec", + ] - day_names = ['', 'måndag', 'tisdag', 'onsdag', 'torsdag', 'fredag', 'lördag', 'söndag'] - day_abbreviations = ['', 'mån', 'tis', 'ons', 'tor', 'fre', 'lör', 'sön'] + day_names = [ + "", + "måndag", + "tisdag", + "onsdag", + "torsdag", + "fredag", + "lördag", + "söndag", + ] + day_abbreviations = ["", "mån", "tis", "ons", "tor", "fre", "lör", "sön"] class FinnishLocale(Locale): - names = ['fi', 'fi_fi'] + names = ["fi", "fi_fi"] # The finnish grammar is very complex, and its hard to convert # 1-to-1 to something like English. - past = '{0} sitten' - future = '{0} kuluttua' + past = "{0} sitten" + future = "{0} kuluttua" timeframes = { - 'now': ['juuri nyt', 'juuri nyt'], - 'seconds': ['muutama sekunti', 'muutaman sekunnin'], - 'minute': ['minuutti', 'minuutin'], - 'minutes': ['{0} minuuttia', '{0} minuutin'], - 'hour': ['tunti', 'tunnin'], - 'hours': ['{0} tuntia', '{0} tunnin'], - 'day': ['päivä', 'päivä'], - 'days': ['{0} päivää', '{0} päivän'], - 'month': ['kuukausi', 'kuukauden'], - 'months': ['{0} kuukautta', '{0} kuukauden'], - 'year': ['vuosi', 'vuoden'], - 'years': ['{0} vuotta', '{0} vuoden'], + "now": ["juuri nyt", "juuri nyt"], + "seconds": ["muutama sekunti", "muutaman sekunnin"], + "minute": ["minuutti", "minuutin"], + "minutes": ["{0} minuuttia", "{0} minuutin"], + "hour": ["tunti", "tunnin"], + "hours": ["{0} tuntia", "{0} tunnin"], + "day": ["päivä", "päivä"], + "days": ["{0} päivää", "{0} päivän"], + "month": ["kuukausi", "kuukauden"], + "months": ["{0} kuukautta", "{0} kuukauden"], + "year": ["vuosi", "vuoden"], + "years": ["{0} vuotta", "{0} vuoden"], } # Months and days are lowercase in Finnish - month_names = ['', 'tammikuu', 'helmikuu', 'maaliskuu', 'huhtikuu', - 'toukokuu', 'kesäkuu', 'heinäkuu', 'elokuu', - 'syyskuu', 'lokakuu', 'marraskuu', 'joulukuu'] + month_names = [ + "", + "tammikuu", + "helmikuu", + "maaliskuu", + "huhtikuu", + "toukokuu", + "kesäkuu", + "heinäkuu", + "elokuu", + "syyskuu", + "lokakuu", + "marraskuu", + "joulukuu", + ] - month_abbreviations = ['', 'tammi', 'helmi', 'maalis', 'huhti', - 'touko', 'kesä', 'heinä', 'elo', - 'syys', 'loka', 'marras', 'joulu'] + month_abbreviations = [ + "", + "tammi", + "helmi", + "maalis", + "huhti", + "touko", + "kesä", + "heinä", + "elo", + "syys", + "loka", + "marras", + "joulu", + ] - day_names = ['', 'maanantai', 'tiistai', 'keskiviikko', 'torstai', - 'perjantai', 'lauantai', 'sunnuntai'] + day_names = [ + "", + "maanantai", + "tiistai", + "keskiviikko", + "torstai", + "perjantai", + "lauantai", + "sunnuntai", + ] - day_abbreviations = ['', 'ma', 'ti', 'ke', 'to', 'pe', 'la', 'su'] + day_abbreviations = ["", "ma", "ti", "ke", "to", "pe", "la", "su"] def _format_timeframe(self, timeframe, delta): - return (self.timeframes[timeframe][0].format(abs(delta)), - self.timeframes[timeframe][1].format(abs(delta))) + return ( + self.timeframes[timeframe][0].format(abs(delta)), + self.timeframes[timeframe][1].format(abs(delta)), + ) def _format_relative(self, humanized, timeframe, delta): - if timeframe == 'now': + if timeframe == "now": return humanized[0] direction = self.past if delta < 0 else self.future @@ -498,138 +774,250 @@ def _format_relative(self, humanized, timeframe, delta): return direction.format(humanized[which]) def _ordinal_number(self, n): - return '{}.'.format(n) + return "{}.".format(n) class ChineseCNLocale(Locale): - names = ['zh', 'zh_cn'] + names = ["zh", "zh_cn"] - past = '{0}前' - future = '{0}后' + past = "{0}前" + future = "{0}后" timeframes = { - 'now': '刚才', - 'seconds': '几秒', - 'minute': '1分钟', - 'minutes': '{0}分钟', - 'hour': '1小时', - 'hours': '{0}小时', - 'day': '1天', - 'days': '{0}天', - 'month': '1个月', - 'months': '{0}个月', - 'year': '1年', - 'years': '{0}年', + "now": "刚才", + "seconds": "几秒", + "minute": "1分钟", + "minutes": "{0}分钟", + "hour": "1小时", + "hours": "{0}小时", + "day": "1天", + "days": "{0}天", + "month": "1个月", + "months": "{0}个月", + "year": "1年", + "years": "{0}年", } - month_names = ['', '一月', '二月', '三月', '四月', '五月', '六月', '七月', - '八月', '九月', '十月', '十一月', '十二月'] - month_abbreviations = ['', ' 1', ' 2', ' 3', ' 4', ' 5', ' 6', ' 7', ' 8', - ' 9', '10', '11', '12'] + month_names = [ + "", + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月", + ] + month_abbreviations = [ + "", + " 1", + " 2", + " 3", + " 4", + " 5", + " 6", + " 7", + " 8", + " 9", + "10", + "11", + "12", + ] - day_names = ['', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'] - day_abbreviations = ['', '一', '二', '三', '四', '五', '六', '日'] + day_names = ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] + day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] class ChineseTWLocale(Locale): - names = ['zh_tw'] + names = ["zh_tw"] - past = '{0}前' - future = '{0}後' + past = "{0}前" + future = "{0}後" timeframes = { - 'now': '剛才', - 'seconds': '幾秒', - 'minute': '1分鐘', - 'minutes': '{0}分鐘', - 'hour': '1小時', - 'hours': '{0}小時', - 'day': '1天', - 'days': '{0}天', - 'month': '1個月', - 'months': '{0}個月', - 'year': '1年', - 'years': '{0}年', + "now": "剛才", + "seconds": "幾秒", + "minute": "1分鐘", + "minutes": "{0}分鐘", + "hour": "1小時", + "hours": "{0}小時", + "day": "1天", + "days": "{0}天", + "month": "1個月", + "months": "{0}個月", + "year": "1年", + "years": "{0}年", } - month_names = ['', '1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', - '9月', '10月', '11月', '12月'] - month_abbreviations = ['', ' 1', ' 2', ' 3', ' 4', ' 5', ' 6', ' 7', ' 8', - ' 9', '10', '11', '12'] + month_names = [ + "", + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月", + ] + month_abbreviations = [ + "", + " 1", + " 2", + " 3", + " 4", + " 5", + " 6", + " 7", + " 8", + " 9", + "10", + "11", + "12", + ] - day_names = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日'] - day_abbreviations = ['', '一', '二', '三', '四', '五', '六', '日'] + day_names = ["", "周一", "周二", "周三", "周四", "周五", "周六", "周日"] + day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] class KoreanLocale(Locale): - names = ['ko', 'ko_kr'] + names = ["ko", "ko_kr"] - past = '{0} 전' - future = '{0} 후' + past = "{0} 전" + future = "{0} 후" timeframes = { - 'now': '지금', - 'seconds': '몇 초', - 'minute': '1분', - 'minutes': '{0}분', - 'hour': '1시간', - 'hours': '{0}시간', - 'day': '1일', - 'days': '{0}일', - 'month': '1개월', - 'months': '{0}개월', - 'year': '1년', - 'years': '{0}년', + "now": "지금", + "seconds": "몇 초", + "minute": "1분", + "minutes": "{0}분", + "hour": "1시간", + "hours": "{0}시간", + "day": "1일", + "days": "{0}일", + "month": "1개월", + "months": "{0}개월", + "year": "1년", + "years": "{0}년", } - month_names = ['', '1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', - '9월', '10월', '11월', '12월'] - month_abbreviations = ['', ' 1', ' 2', ' 3', ' 4', ' 5', ' 6', ' 7', ' 8', - ' 9', '10', '11', '12'] + month_names = [ + "", + "1월", + "2월", + "3월", + "4월", + "5월", + "6월", + "7월", + "8월", + "9월", + "10월", + "11월", + "12월", + ] + month_abbreviations = [ + "", + " 1", + " 2", + " 3", + " 4", + " 5", + " 6", + " 7", + " 8", + " 9", + "10", + "11", + "12", + ] - day_names = ['', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'] - day_abbreviations = ['', '월', '화', '수', '목', '금', '토', '일'] + day_names = ["", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"] + day_abbreviations = ["", "월", "화", "수", "목", "금", "토", "일"] # derived locale types & implementations. class DutchLocale(Locale): - names = ['nl', 'nl_nl'] + names = ["nl", "nl_nl"] - past = '{0} geleden' - future = 'over {0}' + past = "{0} geleden" + future = "over {0}" timeframes = { - 'now': 'nu', - 'seconds': 'seconden', - 'minute': 'een minuut', - 'minutes': '{0} minuten', - 'hour': 'een uur', - 'hours': '{0} uur', - 'day': 'een dag', - 'days': '{0} dagen', - 'month': 'een maand', - 'months': '{0} maanden', - 'year': 'een jaar', - 'years': '{0} jaar', + "now": "nu", + "seconds": "seconden", + "minute": "een minuut", + "minutes": "{0} minuten", + "hour": "een uur", + "hours": "{0} uur", + "day": "een dag", + "days": "{0} dagen", + "month": "een maand", + "months": "{0} maanden", + "year": "een jaar", + "years": "{0} jaar", } # In Dutch names of months and days are not starting with a capital letter # like in the English language. - month_names = ['', 'januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', - 'augustus', 'september', 'oktober', 'november', 'december'] - month_abbreviations = ['', 'jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', - 'sep', 'okt', 'nov', 'dec'] + month_names = [ + "", + "januari", + "februari", + "maart", + "april", + "mei", + "juni", + "juli", + "augustus", + "september", + "oktober", + "november", + "december", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mrt", + "apr", + "mei", + "jun", + "jul", + "aug", + "sep", + "okt", + "nov", + "dec", + ] - day_names = ['', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag', 'zondag'] - day_abbreviations = ['', 'ma', 'di', 'wo', 'do', 'vr', 'za', 'zo'] + day_names = [ + "", + "maandag", + "dinsdag", + "woensdag", + "donderdag", + "vrijdag", + "zaterdag", + "zondag", + ] + day_abbreviations = ["", "ma", "di", "wo", "do", "vr", "za", "zo"] class SlavicBaseLocale(Locale): - def _format_timeframe(self, timeframe, delta): form = self.timeframes[timeframe] @@ -646,221 +1034,419 @@ def _format_timeframe(self, timeframe, delta): return form.format(delta) + class BelarusianLocale(SlavicBaseLocale): - names = ['be', 'be_by'] + names = ["be", "be_by"] - past = '{0} таму' - future = 'праз {0}' + past = "{0} таму" + future = "праз {0}" timeframes = { - 'now': 'зараз', - 'seconds': 'некалькі секунд', - 'minute': 'хвіліну', - 'minutes': ['{0} хвіліну', '{0} хвіліны', '{0} хвілін'], - 'hour': 'гадзіну', - 'hours': ['{0} гадзіну', '{0} гадзіны', '{0} гадзін'], - 'day': 'дзень', - 'days': ['{0} дзень', '{0} дні', '{0} дзён'], - 'month': 'месяц', - 'months': ['{0} месяц', '{0} месяцы', '{0} месяцаў'], - 'year': 'год', - 'years': ['{0} год', '{0} гады', '{0} гадоў'], + "now": "зараз", + "seconds": "некалькі секунд", + "minute": "хвіліну", + "minutes": ["{0} хвіліну", "{0} хвіліны", "{0} хвілін"], + "hour": "гадзіну", + "hours": ["{0} гадзіну", "{0} гадзіны", "{0} гадзін"], + "day": "дзень", + "days": ["{0} дзень", "{0} дні", "{0} дзён"], + "month": "месяц", + "months": ["{0} месяц", "{0} месяцы", "{0} месяцаў"], + "year": "год", + "years": ["{0} год", "{0} гады", "{0} гадоў"], } - month_names = ['', 'студзеня', 'лютага', 'сакавіка', 'красавіка', 'траўня', 'чэрвеня', - 'ліпеня', 'жніўня', 'верасня', 'кастрычніка', 'лістапада', 'снежня'] - month_abbreviations = ['', 'студ', 'лют', 'сак', 'крас', 'трав', 'чэрв', 'ліп', 'жнів', - 'вер', 'каст', 'ліст', 'снеж'] + month_names = [ + "", + "студзеня", + "лютага", + "сакавіка", + "красавіка", + "траўня", + "чэрвеня", + "ліпеня", + "жніўня", + "верасня", + "кастрычніка", + "лістапада", + "снежня", + ] + month_abbreviations = [ + "", + "студ", + "лют", + "сак", + "крас", + "трав", + "чэрв", + "ліп", + "жнів", + "вер", + "каст", + "ліст", + "снеж", + ] - day_names = ['', 'панядзелак', 'аўторак', 'серада', 'чацвер', 'пятніца', 'субота', 'нядзеля'] - day_abbreviations = ['', 'пн', 'ат', 'ср', 'чц', 'пт', 'сб', 'нд'] + day_names = [ + "", + "панядзелак", + "аўторак", + "серада", + "чацвер", + "пятніца", + "субота", + "нядзеля", + ] + day_abbreviations = ["", "пн", "ат", "ср", "чц", "пт", "сб", "нд"] class PolishLocale(SlavicBaseLocale): - names = ['pl', 'pl_pl'] + names = ["pl", "pl_pl"] - past = '{0} temu' - future = 'za {0}' + past = "{0} temu" + future = "za {0}" timeframes = { - 'now': 'teraz', - 'seconds': 'kilka sekund', - 'minute': 'minutę', - 'minutes': ['{0} minut', '{0} minuty', '{0} minut'], - 'hour': 'godzina', - 'hours': ['{0} godzin', '{0} godziny', '{0} godzin'], - 'day': 'dzień', - 'days': ['{0} dzień', '{0} dni', '{0} dni'], - 'month': 'miesiąc', - 'months': ['{0} miesiąc', '{0} miesiące', '{0} miesięcy'], - 'year': 'rok', - 'years': ['{0} rok', '{0} lata', '{0} lat'], + "now": "teraz", + "seconds": "kilka sekund", + "minute": "minutę", + "minutes": ["{0} minut", "{0} minuty", "{0} minut"], + "hour": "godzina", + "hours": ["{0} godzin", "{0} godziny", "{0} godzin"], + "day": "dzień", + "days": ["{0} dzień", "{0} dni", "{0} dni"], + "month": "miesiąc", + "months": ["{0} miesiąc", "{0} miesiące", "{0} miesięcy"], + "year": "rok", + "years": ["{0} rok", "{0} lata", "{0} lat"], } - month_names = ['', 'styczeń', 'luty', 'marzec', 'kwiecień', 'maj', - 'czerwiec', 'lipiec', 'sierpień', 'wrzesień', 'październik', - 'listopad', 'grudzień'] - month_abbreviations = ['', 'sty', 'lut', 'mar', 'kwi', 'maj', 'cze', 'lip', - 'sie', 'wrz', 'paź', 'lis', 'gru'] + month_names = [ + "", + "styczeń", + "luty", + "marzec", + "kwiecień", + "maj", + "czerwiec", + "lipiec", + "sierpień", + "wrzesień", + "październik", + "listopad", + "grudzień", + ] + month_abbreviations = [ + "", + "sty", + "lut", + "mar", + "kwi", + "maj", + "cze", + "lip", + "sie", + "wrz", + "paź", + "lis", + "gru", + ] - day_names = ['', 'poniedziałek', 'wtorek', 'środa', 'czwartek', 'piątek', - 'sobota', 'niedziela'] - day_abbreviations = ['', 'Pn', 'Wt', 'Śr', 'Czw', 'Pt', 'So', 'Nd'] + day_names = [ + "", + "poniedziałek", + "wtorek", + "środa", + "czwartek", + "piątek", + "sobota", + "niedziela", + ] + day_abbreviations = ["", "Pn", "Wt", "Śr", "Czw", "Pt", "So", "Nd"] class RussianLocale(SlavicBaseLocale): - names = ['ru', 'ru_ru'] + names = ["ru", "ru_ru"] - past = '{0} назад' - future = 'через {0}' + past = "{0} назад" + future = "через {0}" timeframes = { - 'now': 'сейчас', - 'seconds': 'несколько секунд', - 'minute': 'минуту', - 'minutes': ['{0} минуту', '{0} минуты', '{0} минут'], - 'hour': 'час', - 'hours': ['{0} час', '{0} часа', '{0} часов'], - 'day': 'день', - 'days': ['{0} день', '{0} дня', '{0} дней'], - 'month': 'месяц', - 'months': ['{0} месяц', '{0} месяца', '{0} месяцев'], - 'year': 'год', - 'years': ['{0} год', '{0} года', '{0} лет'], + "now": "сейчас", + "seconds": "несколько секунд", + "minute": "минуту", + "minutes": ["{0} минуту", "{0} минуты", "{0} минут"], + "hour": "час", + "hours": ["{0} час", "{0} часа", "{0} часов"], + "day": "день", + "days": ["{0} день", "{0} дня", "{0} дней"], + "month": "месяц", + "months": ["{0} месяц", "{0} месяца", "{0} месяцев"], + "year": "год", + "years": ["{0} год", "{0} года", "{0} лет"], } - month_names = ['', 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', - 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'] - month_abbreviations = ['', 'янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', - 'авг', 'сен', 'окт', 'ноя', 'дек'] + month_names = [ + "", + "января", + "февраля", + "марта", + "апреля", + "мая", + "июня", + "июля", + "августа", + "сентября", + "октября", + "ноября", + "декабря", + ] + month_abbreviations = [ + "", + "янв", + "фев", + "мар", + "апр", + "май", + "июн", + "июл", + "авг", + "сен", + "окт", + "ноя", + "дек", + ] - day_names = ['', 'понедельник', 'вторник', 'среда', 'четверг', 'пятница', - 'суббота', 'воскресенье'] - day_abbreviations = ['', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб', 'вс'] + day_names = [ + "", + "понедельник", + "вторник", + "среда", + "четверг", + "пятница", + "суббота", + "воскресенье", + ] + day_abbreviations = ["", "пн", "вт", "ср", "чт", "пт", "сб", "вс"] class BulgarianLocale(SlavicBaseLocale): - names = ['bg', 'bg_BG'] + names = ["bg", "bg_BG"] - past = '{0} назад' - future = 'напред {0}' + past = "{0} назад" + future = "напред {0}" timeframes = { - 'now': 'сега', - 'seconds': 'няколко секунди', - 'minute': 'минута', - 'minutes': ['{0} минута', '{0} минути', '{0} минути'], - 'hour': 'час', - 'hours': ['{0} час', '{0} часа', '{0} часа'], - 'day': 'ден', - 'days': ['{0} ден', '{0} дни', '{0} дни'], - 'month': 'месец', - 'months': ['{0} месец', '{0} месеца', '{0} месеца'], - 'year': 'година', - 'years': ['{0} година', '{0} години', '{0} години'], + "now": "сега", + "seconds": "няколко секунди", + "minute": "минута", + "minutes": ["{0} минута", "{0} минути", "{0} минути"], + "hour": "час", + "hours": ["{0} час", "{0} часа", "{0} часа"], + "day": "ден", + "days": ["{0} ден", "{0} дни", "{0} дни"], + "month": "месец", + "months": ["{0} месец", "{0} месеца", "{0} месеца"], + "year": "година", + "years": ["{0} година", "{0} години", "{0} години"], } - month_names = ['', 'януари', 'февруари', 'март', 'април', 'май', 'юни', - 'юли', 'август', 'септември', 'октомври', 'ноември', 'декември'] - month_abbreviations = ['', 'ян', 'февр', 'март', 'апр', 'май', 'юни', 'юли', - 'авг', 'септ', 'окт', 'ноем', 'дек'] + month_names = [ + "", + "януари", + "февруари", + "март", + "април", + "май", + "юни", + "юли", + "август", + "септември", + "октомври", + "ноември", + "декември", + ] + month_abbreviations = [ + "", + "ян", + "февр", + "март", + "апр", + "май", + "юни", + "юли", + "авг", + "септ", + "окт", + "ноем", + "дек", + ] - day_names = ['', 'понеделник', 'вторник', 'сряда', 'четвъртък', 'петък', - 'събота', 'неделя'] - day_abbreviations = ['', 'пон', 'вт', 'ср', 'четв', 'пет', 'съб', 'нед'] + day_names = [ + "", + "понеделник", + "вторник", + "сряда", + "четвъртък", + "петък", + "събота", + "неделя", + ] + day_abbreviations = ["", "пон", "вт", "ср", "четв", "пет", "съб", "нед"] class UkrainianLocale(SlavicBaseLocale): - names = ['ua', 'uk_ua'] + names = ["ua", "uk_ua"] - past = '{0} тому' - future = 'за {0}' + past = "{0} тому" + future = "за {0}" timeframes = { - 'now': 'зараз', - 'seconds': 'кілька секунд', - 'minute': 'хвилину', - 'minutes': ['{0} хвилину', '{0} хвилини', '{0} хвилин'], - 'hour': 'годину', - 'hours': ['{0} годину', '{0} години', '{0} годин'], - 'day': 'день', - 'days': ['{0} день', '{0} дні', '{0} днів'], - 'month': 'місяць', - 'months': ['{0} місяць', '{0} місяці', '{0} місяців'], - 'year': 'рік', - 'years': ['{0} рік', '{0} роки', '{0} років'], + "now": "зараз", + "seconds": "кілька секунд", + "minute": "хвилину", + "minutes": ["{0} хвилину", "{0} хвилини", "{0} хвилин"], + "hour": "годину", + "hours": ["{0} годину", "{0} години", "{0} годин"], + "day": "день", + "days": ["{0} день", "{0} дні", "{0} днів"], + "month": "місяць", + "months": ["{0} місяць", "{0} місяці", "{0} місяців"], + "year": "рік", + "years": ["{0} рік", "{0} роки", "{0} років"], } - month_names = ['', 'січня', 'лютого', 'березня', 'квітня', 'травня', 'червня', - 'липня', 'серпня', 'вересня', 'жовтня', 'листопада', 'грудня'] - month_abbreviations = ['', 'січ', 'лют', 'бер', 'квіт', 'трав', 'черв', 'лип', 'серп', - 'вер', 'жовт', 'лист', 'груд'] + month_names = [ + "", + "січня", + "лютого", + "березня", + "квітня", + "травня", + "червня", + "липня", + "серпня", + "вересня", + "жовтня", + "листопада", + "грудня", + ] + month_abbreviations = [ + "", + "січ", + "лют", + "бер", + "квіт", + "трав", + "черв", + "лип", + "серп", + "вер", + "жовт", + "лист", + "груд", + ] - day_names = ['', 'понеділок', 'вівторок', 'середа', 'четвер', 'п’ятниця', 'субота', 'неділя'] - day_abbreviations = ['', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб', 'нд'] + day_names = [ + "", + "понеділок", + "вівторок", + "середа", + "четвер", + "п’ятниця", + "субота", + "неділя", + ] + day_abbreviations = ["", "пн", "вт", "ср", "чт", "пт", "сб", "нд"] class _DeutschLocaleCommonMixin(object): - past = 'vor {0}' - future = 'in {0}' + past = "vor {0}" + future = "in {0}" timeframes = { - 'now': 'gerade eben', - 'seconds': 'Sekunden', - 'minute': 'einer Minute', - 'minutes': '{0} Minuten', - 'hour': 'einer Stunde', - 'hours': '{0} Stunden', - 'day': 'einem Tag', - 'days': '{0} Tagen', - 'month': 'einem Monat', - 'months': '{0} Monaten', - 'year': 'einem Jahr', - 'years': '{0} Jahren', + "now": "gerade eben", + "seconds": "Sekunden", + "minute": "einer Minute", + "minutes": "{0} Minuten", + "hour": "einer Stunde", + "hours": "{0} Stunden", + "day": "einem Tag", + "days": "{0} Tagen", + "month": "einem Monat", + "months": "{0} Monaten", + "year": "einem Jahr", + "years": "{0} Jahren", } timeframes_only_distance = timeframes.copy() - timeframes_only_distance['minute'] = 'eine Minute' - timeframes_only_distance['hour'] = 'eine Stunde' - timeframes_only_distance['day'] = 'ein Tag' - timeframes_only_distance['month'] = 'ein Monat' - timeframes_only_distance['year'] = 'ein Jahr' + timeframes_only_distance["minute"] = "eine Minute" + timeframes_only_distance["hour"] = "eine Stunde" + timeframes_only_distance["day"] = "ein Tag" + timeframes_only_distance["month"] = "ein Monat" + timeframes_only_distance["year"] = "ein Jahr" month_names = [ - '', 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', - 'August', 'September', 'Oktober', 'November', 'Dezember' + "", + "Januar", + "Februar", + "März", + "April", + "Mai", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "Dezember", ] month_abbreviations = [ - '', 'Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', - 'Okt', 'Nov', 'Dez' + "", + "Jan", + "Feb", + "Mär", + "Apr", + "Mai", + "Jun", + "Jul", + "Aug", + "Sep", + "Okt", + "Nov", + "Dez", ] day_names = [ - '', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', - 'Samstag', 'Sonntag' + "", + "Montag", + "Dienstag", + "Mittwoch", + "Donnerstag", + "Freitag", + "Samstag", + "Sonntag", ] - day_abbreviations = [ - '', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So' - ] + day_abbreviations = ["", "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] def _ordinal_number(self, n): - return '{}.'.format(n) + return "{}.".format(n) def describe(self, timeframe, delta=0, only_distance=False): - ''' Describes a delta within a timeframe in plain language. + """ Describes a delta within a timeframe in plain language. :param timeframe: a string representing a timeframe. :param delta: a quantity representing a delta in a timeframe. :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords - ''' + """ humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) @@ -873,345 +1459,734 @@ def describe(self, timeframe, delta=0, only_distance=False): class GermanLocale(_DeutschLocaleCommonMixin, Locale): - names = ['de', 'de_de'] + names = ["de", "de_de"] class AustrianLocale(_DeutschLocaleCommonMixin, Locale): - names = ['de_at'] + names = ["de_at"] month_names = [ - '', 'Jänner', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', - 'August', 'September', 'Oktober', 'November', 'Dezember' + "", + "Jänner", + "Februar", + "März", + "April", + "Mai", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "Dezember", ] class NorwegianLocale(Locale): - names = ['nb', 'nb_no'] + names = ["nb", "nb_no"] - past = 'for {0} siden' - future = 'om {0}' + past = "for {0} siden" + future = "om {0}" timeframes = { - 'now': 'nå nettopp', - 'seconds': 'noen sekunder', - 'minute': 'ett minutt', - 'minutes': '{0} minutter', - 'hour': 'en time', - 'hours': '{0} timer', - 'day': 'en dag', - 'days': '{0} dager', - 'month': 'en måned', - 'months': '{0} måneder', - 'year': 'ett år', - 'years': '{0} år', + "now": "nå nettopp", + "seconds": "noen sekunder", + "minute": "ett minutt", + "minutes": "{0} minutter", + "hour": "en time", + "hours": "{0} timer", + "day": "en dag", + "days": "{0} dager", + "month": "en måned", + "months": "{0} måneder", + "year": "ett år", + "years": "{0} år", } - month_names = ['', 'januar', 'februar', 'mars', 'april', 'mai', 'juni', - 'juli', 'august', 'september', 'oktober', 'november', - 'desember'] - month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'mai', 'jun', 'jul', - 'aug', 'sep', 'okt', 'nov', 'des'] + month_names = [ + "", + "januar", + "februar", + "mars", + "april", + "mai", + "juni", + "juli", + "august", + "september", + "oktober", + "november", + "desember", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mar", + "apr", + "mai", + "jun", + "jul", + "aug", + "sep", + "okt", + "nov", + "des", + ] - day_names = ['', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', - 'lørdag', 'søndag'] - day_abbreviations = ['', 'ma', 'ti', 'on', 'to', 'fr', 'lø', 'sø'] + day_names = [ + "", + "mandag", + "tirsdag", + "onsdag", + "torsdag", + "fredag", + "lørdag", + "søndag", + ] + day_abbreviations = ["", "ma", "ti", "on", "to", "fr", "lø", "sø"] class NewNorwegianLocale(Locale): - names = ['nn', 'nn_no'] + names = ["nn", "nn_no"] - past = 'for {0} sidan' - future = 'om {0}' + past = "for {0} sidan" + future = "om {0}" timeframes = { - 'now': 'no nettopp', - 'seconds': 'nokre sekund', - 'minute': 'ett minutt', - 'minutes': '{0} minutt', - 'hour': 'ein time', - 'hours': '{0} timar', - 'day': 'ein dag', - 'days': '{0} dagar', - 'month': 'en månad', - 'months': '{0} månader', - 'year': 'eit år', - 'years': '{0} år', + "now": "no nettopp", + "seconds": "nokre sekund", + "minute": "ett minutt", + "minutes": "{0} minutt", + "hour": "ein time", + "hours": "{0} timar", + "day": "ein dag", + "days": "{0} dagar", + "month": "en månad", + "months": "{0} månader", + "year": "eit år", + "years": "{0} år", } - month_names = ['', 'januar', 'februar', 'mars', 'april', 'mai', 'juni', - 'juli', 'august', 'september', 'oktober', 'november', - 'desember'] - month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'mai', 'jun', 'jul', - 'aug', 'sep', 'okt', 'nov', 'des'] + month_names = [ + "", + "januar", + "februar", + "mars", + "april", + "mai", + "juni", + "juli", + "august", + "september", + "oktober", + "november", + "desember", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mar", + "apr", + "mai", + "jun", + "jul", + "aug", + "sep", + "okt", + "nov", + "des", + ] - day_names = ['', 'måndag', 'tysdag', 'onsdag', 'torsdag', 'fredag', - 'laurdag', 'sundag'] - day_abbreviations = ['', 'må', 'ty', 'on', 'to', 'fr', 'la', 'su'] + day_names = [ + "", + "måndag", + "tysdag", + "onsdag", + "torsdag", + "fredag", + "laurdag", + "sundag", + ] + day_abbreviations = ["", "må", "ty", "on", "to", "fr", "la", "su"] class PortugueseLocale(Locale): - names = ['pt', 'pt_pt'] + names = ["pt", "pt_pt"] - past = 'há {0}' - future = 'em {0}' + past = "há {0}" + future = "em {0}" timeframes = { - 'now': 'agora', - 'seconds': 'segundos', - 'minute': 'um minuto', - 'minutes': '{0} minutos', - 'hour': 'uma hora', - 'hours': '{0} horas', - 'day': 'um dia', - 'days': '{0} dias', - 'month': 'um mês', - 'months': '{0} meses', - 'year': 'um ano', - 'years': '{0} anos', + "now": "agora", + "seconds": "segundos", + "minute": "um minuto", + "minutes": "{0} minutos", + "hour": "uma hora", + "hours": "{0} horas", + "day": "um dia", + "days": "{0} dias", + "month": "um mês", + "months": "{0} meses", + "year": "um ano", + "years": "{0} anos", } - month_names = ['', 'janeiro', 'fevereiro', 'março', 'abril', 'maio', 'junho', 'julho', - 'agosto', 'setembro', 'outubro', 'novembro', 'dezembro'] - month_abbreviations = ['', 'jan', 'fev', 'mar', 'abr', 'maio', 'jun', 'jul', 'ago', - 'set', 'out', 'nov', 'dez'] + month_names = [ + "", + "janeiro", + "fevereiro", + "março", + "abril", + "maio", + "junho", + "julho", + "agosto", + "setembro", + "outubro", + "novembro", + "dezembro", + ] + month_abbreviations = [ + "", + "jan", + "fev", + "mar", + "abr", + "maio", + "jun", + "jul", + "ago", + "set", + "out", + "nov", + "dez", + ] - day_names = ['', 'segunda-feira', 'terça-feira', 'quarta-feira', 'quinta-feira', 'sexta-feira', - 'sábado', 'domingo'] - day_abbreviations = ['', 'seg', 'ter', 'qua', 'qui', 'sex', 'sab', 'dom'] + day_names = [ + "", + "segunda-feira", + "terça-feira", + "quarta-feira", + "quinta-feira", + "sexta-feira", + "sábado", + "domingo", + ] + day_abbreviations = ["", "seg", "ter", "qua", "qui", "sex", "sab", "dom"] class BrazilianPortugueseLocale(PortugueseLocale): - names = ['pt_br'] + names = ["pt_br"] - past = 'faz {0}' + past = "faz {0}" class TagalogLocale(Locale): - names = ['tl', 'tl_ph'] + names = ["tl", "tl_ph"] - past = 'nakaraang {0}' - future = '{0} mula ngayon' + past = "nakaraang {0}" + future = "{0} mula ngayon" timeframes = { - 'now': 'ngayon lang', - 'seconds': 'segundo', - 'minute': 'isang minuto', - 'minutes': '{0} minuto', - 'hour': 'isang oras', - 'hours': '{0} oras', - 'day': 'isang araw', - 'days': '{0} araw', - 'month': 'isang buwan', - 'months': '{0} buwan', - 'year': 'isang taon', - 'years': '{0} taon', + "now": "ngayon lang", + "seconds": "segundo", + "minute": "isang minuto", + "minutes": "{0} minuto", + "hour": "isang oras", + "hours": "{0} oras", + "day": "isang araw", + "days": "{0} araw", + "month": "isang buwan", + "months": "{0} buwan", + "year": "isang taon", + "years": "{0} taon", } - month_names = ['', 'Enero', 'Pebrero', 'Marso', 'Abril', 'Mayo', 'Hunyo', 'Hulyo', - 'Agosto', 'Setyembre', 'Oktubre', 'Nobyembre', 'Disyembre'] - month_abbreviations = ['', 'Ene', 'Peb', 'Mar', 'Abr', 'May', 'Hun', 'Hul', 'Ago', - 'Set', 'Okt', 'Nob', 'Dis'] + month_names = [ + "", + "Enero", + "Pebrero", + "Marso", + "Abril", + "Mayo", + "Hunyo", + "Hulyo", + "Agosto", + "Setyembre", + "Oktubre", + "Nobyembre", + "Disyembre", + ] + month_abbreviations = [ + "", + "Ene", + "Peb", + "Mar", + "Abr", + "May", + "Hun", + "Hul", + "Ago", + "Set", + "Okt", + "Nob", + "Dis", + ] - day_names = ['', 'Lunes', 'Martes', 'Miyerkules', 'Huwebes', 'Biyernes', 'Sabado', 'Linggo'] - day_abbreviations = ['', 'Lun', 'Mar', 'Miy', 'Huw', 'Biy', 'Sab', 'Lin'] + day_names = [ + "", + "Lunes", + "Martes", + "Miyerkules", + "Huwebes", + "Biyernes", + "Sabado", + "Linggo", + ] + day_abbreviations = ["", "Lun", "Mar", "Miy", "Huw", "Biy", "Sab", "Lin"] def _ordinal_number(self, n): - return 'ika-{}'.format(n) + return "ika-{}".format(n) + class VietnameseLocale(Locale): - names = ['vi', 'vi_vn'] + names = ["vi", "vi_vn"] - past = '{0} trước' - future = '{0} nữa' + past = "{0} trước" + future = "{0} nữa" timeframes = { - 'now': 'hiện tại', - 'seconds': 'giây', - 'minute': 'một phút', - 'minutes': '{0} phút', - 'hour': 'một giờ', - 'hours': '{0} giờ', - 'day': 'một ngày', - 'days': '{0} ngày', - 'month': 'một tháng', - 'months': '{0} tháng', - 'year': 'một năm', - 'years': '{0} năm', + "now": "hiện tại", + "seconds": "giây", + "minute": "một phút", + "minutes": "{0} phút", + "hour": "một giờ", + "hours": "{0} giờ", + "day": "một ngày", + "days": "{0} ngày", + "month": "một tháng", + "months": "{0} tháng", + "year": "một năm", + "years": "{0} năm", } - month_names = ['', 'Tháng Một', 'Tháng Hai', 'Tháng Ba', 'Tháng Tư', 'Tháng Năm', 'Tháng Sáu', 'Tháng Bảy', - 'Tháng Tám', 'Tháng Chín', 'Tháng Mười', 'Tháng Mười Một', 'Tháng Mười Hai'] - month_abbreviations = ['', 'Tháng 1', 'Tháng 2', 'Tháng 3', 'Tháng 4', 'Tháng 5', 'Tháng 6', 'Tháng 7', 'Tháng 8', - 'Tháng 9', 'Tháng 10', 'Tháng 11', 'Tháng 12'] + month_names = [ + "", + "Tháng Một", + "Tháng Hai", + "Tháng Ba", + "Tháng Tư", + "Tháng Năm", + "Tháng Sáu", + "Tháng Bảy", + "Tháng Tám", + "Tháng Chín", + "Tháng Mười", + "Tháng Mười Một", + "Tháng Mười Hai", + ] + month_abbreviations = [ + "", + "Tháng 1", + "Tháng 2", + "Tháng 3", + "Tháng 4", + "Tháng 5", + "Tháng 6", + "Tháng 7", + "Tháng 8", + "Tháng 9", + "Tháng 10", + "Tháng 11", + "Tháng 12", + ] - day_names = ['', 'Thứ Hai', 'Thứ Ba', 'Thứ Tư', 'Thứ Năm', 'Thứ Sáu', 'Thứ Bảy', 'Chủ Nhật'] - day_abbreviations = ['', 'Thứ 2', 'Thứ 3', 'Thứ 4', 'Thứ 5', 'Thứ 6', 'Thứ 7', 'CN'] + day_names = [ + "", + "Thứ Hai", + "Thứ Ba", + "Thứ Tư", + "Thứ Năm", + "Thứ Sáu", + "Thứ Bảy", + "Chủ Nhật", + ] + day_abbreviations = ["", "Thứ 2", "Thứ 3", "Thứ 4", "Thứ 5", "Thứ 6", "Thứ 7", "CN"] class TurkishLocale(Locale): - names = ['tr', 'tr_tr'] + names = ["tr", "tr_tr"] - past = '{0} önce' - future = '{0} sonra' + past = "{0} önce" + future = "{0} sonra" timeframes = { - 'now': 'şimdi', - 'seconds': 'saniye', - 'minute': 'bir dakika', - 'minutes': '{0} dakika', - 'hour': 'bir saat', - 'hours': '{0} saat', - 'day': 'bir gün', - 'days': '{0} gün', - 'month': 'bir ay', - 'months': '{0} ay', - 'year': 'yıl', - 'years': '{0} yıl', + "now": "şimdi", + "seconds": "saniye", + "minute": "bir dakika", + "minutes": "{0} dakika", + "hour": "bir saat", + "hours": "{0} saat", + "day": "bir gün", + "days": "{0} gün", + "month": "bir ay", + "months": "{0} ay", + "year": "yıl", + "years": "{0} yıl", } - month_names = ['', 'Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', 'Haziran', 'Temmuz', - 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'] - month_abbreviations = ['', 'Oca', 'Şub', 'Mar', 'Nis', 'May', 'Haz', 'Tem', 'Ağu', - 'Eyl', 'Eki', 'Kas', 'Ara'] + month_names = [ + "", + "Ocak", + "Şubat", + "Mart", + "Nisan", + "Mayıs", + "Haziran", + "Temmuz", + "Ağustos", + "Eylül", + "Ekim", + "Kasım", + "Aralık", + ] + month_abbreviations = [ + "", + "Oca", + "Şub", + "Mar", + "Nis", + "May", + "Haz", + "Tem", + "Ağu", + "Eyl", + "Eki", + "Kas", + "Ara", + ] - day_names = ['', 'Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi', 'Pazar'] - day_abbreviations = ['', 'Pzt', 'Sal', 'Çar', 'Per', 'Cum', 'Cmt', 'Paz'] + day_names = [ + "", + "Pazartesi", + "Salı", + "Çarşamba", + "Perşembe", + "Cuma", + "Cumartesi", + "Pazar", + ] + day_abbreviations = ["", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt", "Paz"] class AzerbaijaniLocale(Locale): - names = ['az', 'az_az'] + names = ["az", "az_az"] - past = '{0} əvvəl' - future = '{0} sonra' + past = "{0} əvvəl" + future = "{0} sonra" timeframes = { - 'now': 'indi', - 'seconds': 'saniyə', - 'minute': 'bir dəqiqə', - 'minutes': '{0} dəqiqə', - 'hour': 'bir saat', - 'hours': '{0} saat', - 'day': 'bir gün', - 'days': '{0} gün', - 'month': 'bir ay', - 'months': '{0} ay', - 'year': 'il', - 'years': '{0} il', + "now": "indi", + "seconds": "saniyə", + "minute": "bir dəqiqə", + "minutes": "{0} dəqiqə", + "hour": "bir saat", + "hours": "{0} saat", + "day": "bir gün", + "days": "{0} gün", + "month": "bir ay", + "months": "{0} ay", + "year": "il", + "years": "{0} il", } - month_names = ['', 'Yanvar', 'Fevral', 'Mart', 'Aprel', 'May', 'İyun', 'İyul', - 'Avqust', 'Sentyabr', 'Oktyabr', 'Noyabr', 'Dekabr'] - month_abbreviations = ['', 'Yan', 'Fev', 'Mar', 'Apr', 'May', 'İyn', 'İyl', 'Avq', - 'Sen', 'Okt', 'Noy', 'Dek'] + month_names = [ + "", + "Yanvar", + "Fevral", + "Mart", + "Aprel", + "May", + "İyun", + "İyul", + "Avqust", + "Sentyabr", + "Oktyabr", + "Noyabr", + "Dekabr", + ] + month_abbreviations = [ + "", + "Yan", + "Fev", + "Mar", + "Apr", + "May", + "İyn", + "İyl", + "Avq", + "Sen", + "Okt", + "Noy", + "Dek", + ] + + day_names = [ + "", + "Bazar ertəsi", + "Çərşənbə axşamı", + "Çərşənbə", + "Cümə axşamı", + "Cümə", + "Şənbə", + "Bazar", + ] + day_abbreviations = ["", "Ber", "Çax", "Çər", "Cax", "Cüm", "Şnb", "Bzr"] - day_names = ['', 'Bazar ertəsi', 'Çərşənbə axşamı', 'Çərşənbə', 'Cümə axşamı', 'Cümə', 'Şənbə', 'Bazar'] - day_abbreviations = ['', 'Ber', 'Çax', 'Çər', 'Cax', 'Cüm', 'Şnb', 'Bzr'] class ArabicLocale(Locale): - names = ['ar', 'ar_ae', 'ar_bh', 'ar_dj', 'ar_eg', 'ar_eh', 'ar_er', - 'ar_km', 'ar_kw', 'ar_ly', 'ar_om', 'ar_qa', 'ar_sa', 'ar_sd', 'ar_so', - 'ar_ss', 'ar_td', 'ar_ye'] + names = [ + "ar", + "ar_ae", + "ar_bh", + "ar_dj", + "ar_eg", + "ar_eh", + "ar_er", + "ar_km", + "ar_kw", + "ar_ly", + "ar_om", + "ar_qa", + "ar_sa", + "ar_sd", + "ar_so", + "ar_ss", + "ar_td", + "ar_ye", + ] - past = 'منذ {0}' - future = 'خلال {0}' + past = "منذ {0}" + future = "خلال {0}" timeframes = { - 'now': 'الآن', - 'seconds': { - 'double' : 'ثانيتين', - 'ten' : '{0} ثوان', - 'higher' : '{0} ثانية' - }, - 'minute': 'دقيقة', - 'minutes': { - 'double' : 'دقيقتين', - 'ten' : '{0} دقائق', - 'higher' : '{0} دقيقة' - }, - 'hour': 'ساعة', - 'hours': { - 'double' : 'ساعتين', - 'ten' : '{0} ساعات', - 'higher' : '{0} ساعة' - }, - 'day': 'يوم', - 'days': { - 'double' : 'يومين', - 'ten' : '{0} أيام', - 'higher' : '{0} يوم' - }, - 'month': 'شهر', - 'months': { - 'double' : 'شهرين', - 'ten' : '{0} أشهر', - 'higher' : '{0} شهر' - }, - 'year': 'سنة', - 'years': { - 'double' : 'سنتين', - 'ten' : '{0} سنوات', - 'higher' : '{0} سنة' - }, + "now": "الآن", + "seconds": {"double": "ثانيتين", "ten": "{0} ثوان", "higher": "{0} ثانية"}, + "minute": "دقيقة", + "minutes": {"double": "دقيقتين", "ten": "{0} دقائق", "higher": "{0} دقيقة"}, + "hour": "ساعة", + "hours": {"double": "ساعتين", "ten": "{0} ساعات", "higher": "{0} ساعة"}, + "day": "يوم", + "days": {"double": "يومين", "ten": "{0} أيام", "higher": "{0} يوم"}, + "month": "شهر", + "months": {"double": "شهرين", "ten": "{0} أشهر", "higher": "{0} شهر"}, + "year": "سنة", + "years": {"double": "سنتين", "ten": "{0} سنوات", "higher": "{0} سنة"}, } - month_names = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', - 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] - month_abbreviations = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', - 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] + month_names = [ + "", + "يناير", + "فبراير", + "مارس", + "أبريل", + "مايو", + "يونيو", + "يوليو", + "أغسطس", + "سبتمبر", + "أكتوبر", + "نوفمبر", + "ديسمبر", + ] + month_abbreviations = [ + "", + "يناير", + "فبراير", + "مارس", + "أبريل", + "مايو", + "يونيو", + "يوليو", + "أغسطس", + "سبتمبر", + "أكتوبر", + "نوفمبر", + "ديسمبر", + ] - day_names = ['', 'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'] - day_abbreviations = ['', 'إثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت', 'أحد'] + day_names = [ + "", + "الإثنين", + "الثلاثاء", + "الأربعاء", + "الخميس", + "الجمعة", + "السبت", + "الأحد", + ] + day_abbreviations = ["", "إثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت", "أحد"] def _format_timeframe(self, timeframe, delta): form = self.timeframes[timeframe] delta = abs(delta) if isinstance(form, dict): if delta == 2: - form = form['double'] + form = form["double"] elif delta > 2 and delta <= 10: - form = form['ten'] + form = form["ten"] else: - form = form['higher'] + form = form["higher"] return form.format(delta) + class LevantArabicLocale(ArabicLocale): - names = ['ar_iq', 'ar_jo', 'ar_lb', 'ar_ps', 'ar_sy'] - month_names = ['', 'كانون الثاني', 'شباط', 'آذار', 'نيسان', 'أيار', 'حزيران', 'تموز', 'آب', - 'أيلول', 'تشرين الأول', 'تشرين الثاني', 'كانون الأول'] - month_abbreviations = ['', 'كانون الثاني', 'شباط', 'آذار', 'نيسان', 'أيار', 'حزيران', 'تموز', 'آب', - 'أيلول', 'تشرين الأول', 'تشرين الثاني', 'كانون الأول'] + names = ["ar_iq", "ar_jo", "ar_lb", "ar_ps", "ar_sy"] + month_names = [ + "", + "كانون الثاني", + "شباط", + "آذار", + "نيسان", + "أيار", + "حزيران", + "تموز", + "آب", + "أيلول", + "تشرين الأول", + "تشرين الثاني", + "كانون الأول", + ] + month_abbreviations = [ + "", + "كانون الثاني", + "شباط", + "آذار", + "نيسان", + "أيار", + "حزيران", + "تموز", + "آب", + "أيلول", + "تشرين الأول", + "تشرين الثاني", + "كانون الأول", + ] + class AlgeriaTunisiaArabicLocale(ArabicLocale): - names = ['ar_tn', 'ar_dz'] - month_names = ['', 'جانفي', 'فيفري', 'مارس', 'أفريل', 'ماي', 'جوان', - 'جويلية', 'أوت', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] - month_abbreviations = ['', 'جانفي', 'فيفري', 'مارس', 'أفريل', 'ماي', 'جوان', - 'جويلية', 'أوت', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] + names = ["ar_tn", "ar_dz"] + month_names = [ + "", + "جانفي", + "فيفري", + "مارس", + "أفريل", + "ماي", + "جوان", + "جويلية", + "أوت", + "سبتمبر", + "أكتوبر", + "نوفمبر", + "ديسمبر", + ] + month_abbreviations = [ + "", + "جانفي", + "فيفري", + "مارس", + "أفريل", + "ماي", + "جوان", + "جويلية", + "أوت", + "سبتمبر", + "أكتوبر", + "نوفمبر", + "ديسمبر", + ] + class MauritaniaArabicLocale(ArabicLocale): - names = ['ar_mr'] - month_names = ['', 'يناير', 'فبراير', 'مارس', 'إبريل', 'مايو', 'يونيو', - 'يوليو', 'أغشت', 'شتمبر', 'أكتوبر', 'نوفمبر', 'دجمبر'] - month_abbreviations = ['', 'يناير', 'فبراير', 'مارس', 'إبريل', 'مايو', 'يونيو', - 'يوليو', 'أغشت', 'شتمبر', 'أكتوبر', 'نوفمبر', 'دجمبر'] + names = ["ar_mr"] + month_names = [ + "", + "يناير", + "فبراير", + "مارس", + "إبريل", + "مايو", + "يونيو", + "يوليو", + "أغشت", + "شتمبر", + "أكتوبر", + "نوفمبر", + "دجمبر", + ] + month_abbreviations = [ + "", + "يناير", + "فبراير", + "مارس", + "إبريل", + "مايو", + "يونيو", + "يوليو", + "أغشت", + "شتمبر", + "أكتوبر", + "نوفمبر", + "دجمبر", + ] + class MoroccoArabicLocale(ArabicLocale): - names = ['ar_ma'] - month_names = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'ماي', 'يونيو', - 'يوليوز', 'غشت', 'شتنبر', 'أكتوبر', 'نونبر', 'دجنبر'] - month_abbreviations = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'ماي', 'يونيو', - 'يوليوز', 'غشت', 'شتنبر', 'أكتوبر', 'نونبر', 'دجنبر'] + names = ["ar_ma"] + month_names = [ + "", + "يناير", + "فبراير", + "مارس", + "أبريل", + "ماي", + "يونيو", + "يوليوز", + "غشت", + "شتنبر", + "أكتوبر", + "نونبر", + "دجنبر", + ] + month_abbreviations = [ + "", + "يناير", + "فبراير", + "مارس", + "أبريل", + "ماي", + "يونيو", + "يوليوز", + "غشت", + "شتنبر", + "أكتوبر", + "نونبر", + "دجنبر", + ] -class IcelandicLocale(Locale): +class IcelandicLocale(Locale): def _format_timeframe(self, timeframe, delta): timeframe = self.timeframes[timeframe] @@ -1222,210 +2197,354 @@ def _format_timeframe(self, timeframe, delta): return timeframe.format(abs(delta)) - names = ['is', 'is_is'] + names = ["is", "is_is"] - past = 'fyrir {0} síðan' - future = 'eftir {0}' + past = "fyrir {0} síðan" + future = "eftir {0}" timeframes = { - 'now': 'rétt í þessu', - 'seconds': ('nokkrum sekúndum', 'nokkrar sekúndur'), - 'minute': ('einni mínútu', 'eina mínútu'), - 'minutes': ('{0} mínútum', '{0} mínútur'), - 'hour': ('einum tíma', 'einn tíma'), - 'hours': ('{0} tímum', '{0} tíma'), - 'day': ('einum degi', 'einn dag'), - 'days': ('{0} dögum', '{0} daga'), - 'month': ('einum mánuði', 'einn mánuð'), - 'months': ('{0} mánuðum', '{0} mánuði'), - 'year': ('einu ári', 'eitt ár'), - 'years': ('{0} árum', '{0} ár'), - } - - meridians = { - 'am': 'f.h.', - 'pm': 'e.h.', - 'AM': 'f.h.', - 'PM': 'e.h.', - } + "now": "rétt í þessu", + "seconds": ("nokkrum sekúndum", "nokkrar sekúndur"), + "minute": ("einni mínútu", "eina mínútu"), + "minutes": ("{0} mínútum", "{0} mínútur"), + "hour": ("einum tíma", "einn tíma"), + "hours": ("{0} tímum", "{0} tíma"), + "day": ("einum degi", "einn dag"), + "days": ("{0} dögum", "{0} daga"), + "month": ("einum mánuði", "einn mánuð"), + "months": ("{0} mánuðum", "{0} mánuði"), + "year": ("einu ári", "eitt ár"), + "years": ("{0} árum", "{0} ár"), + } + + meridians = {"am": "f.h.", "pm": "e.h.", "AM": "f.h.", "PM": "e.h."} - month_names = ['', 'janúar', 'febrúar', 'mars', 'apríl', 'maí', 'júní', - 'júlí', 'ágúst', 'september', 'október', 'nóvember', 'desember'] - month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'maí', 'jún', - 'júl', 'ágú', 'sep', 'okt', 'nóv', 'des'] + month_names = [ + "", + "janúar", + "febrúar", + "mars", + "apríl", + "maí", + "júní", + "júlí", + "ágúst", + "september", + "október", + "nóvember", + "desember", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mar", + "apr", + "maí", + "jún", + "júl", + "ágú", + "sep", + "okt", + "nóv", + "des", + ] - day_names = ['', 'mánudagur', 'þriðjudagur', 'miðvikudagur', 'fimmtudagur', - 'föstudagur', 'laugardagur', 'sunnudagur'] - day_abbreviations = ['', 'mán', 'þri', 'mið', 'fim', 'fös', 'lau', 'sun'] + day_names = [ + "", + "mánudagur", + "þriðjudagur", + "miðvikudagur", + "fimmtudagur", + "föstudagur", + "laugardagur", + "sunnudagur", + ] + day_abbreviations = ["", "mán", "þri", "mið", "fim", "fös", "lau", "sun"] class DanishLocale(Locale): - names = ['da', 'da_dk'] + names = ["da", "da_dk"] - past = 'for {0} siden' - future = 'efter {0}' + past = "for {0} siden" + future = "efter {0}" timeframes = { - 'now': 'lige nu', - 'seconds': 'et par sekunder', - 'minute': 'et minut', - 'minutes': '{0} minutter', - 'hour': 'en time', - 'hours': '{0} timer', - 'day': 'en dag', - 'days': '{0} dage', - 'month': 'en måned', - 'months': '{0} måneder', - 'year': 'et år', - 'years': '{0} år', + "now": "lige nu", + "seconds": "et par sekunder", + "minute": "et minut", + "minutes": "{0} minutter", + "hour": "en time", + "hours": "{0} timer", + "day": "en dag", + "days": "{0} dage", + "month": "en måned", + "months": "{0} måneder", + "year": "et år", + "years": "{0} år", } - month_names = ['', 'januar', 'februar', 'marts', 'april', 'maj', 'juni', - 'juli', 'august', 'september', 'oktober', 'november', 'december'] - month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'maj', 'jun', - 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'] + month_names = [ + "", + "januar", + "februar", + "marts", + "april", + "maj", + "juni", + "juli", + "august", + "september", + "oktober", + "november", + "december", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mar", + "apr", + "maj", + "jun", + "jul", + "aug", + "sep", + "okt", + "nov", + "dec", + ] - day_names = ['', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', - 'lørdag', 'søndag'] - day_abbreviations = ['', 'man', 'tir', 'ons', 'tor', 'fre', 'lør', 'søn'] + day_names = [ + "", + "mandag", + "tirsdag", + "onsdag", + "torsdag", + "fredag", + "lørdag", + "søndag", + ] + day_abbreviations = ["", "man", "tir", "ons", "tor", "fre", "lør", "søn"] class MalayalamLocale(Locale): - names = ['ml'] + names = ["ml"] - past = '{0} മുമ്പ്' - future = '{0} ശേഷം' + past = "{0} മുമ്പ്" + future = "{0} ശേഷം" timeframes = { - 'now': 'ഇപ്പോൾ', - 'seconds': 'സെക്കന്റ്‌', - 'minute': 'ഒരു മിനിറ്റ്', - 'minutes': '{0} മിനിറ്റ്', - 'hour': 'ഒരു മണിക്കൂർ', - 'hours': '{0} മണിക്കൂർ', - 'day': 'ഒരു ദിവസം ', - 'days': '{0} ദിവസം ', - 'month': 'ഒരു മാസം ', - 'months': '{0} മാസം ', - 'year': 'ഒരു വർഷം ', - 'years': '{0} വർഷം ', + "now": "ഇപ്പോൾ", + "seconds": "സെക്കന്റ്‌", + "minute": "ഒരു മിനിറ്റ്", + "minutes": "{0} മിനിറ്റ്", + "hour": "ഒരു മണിക്കൂർ", + "hours": "{0} മണിക്കൂർ", + "day": "ഒരു ദിവസം ", + "days": "{0} ദിവസം ", + "month": "ഒരു മാസം ", + "months": "{0} മാസം ", + "year": "ഒരു വർഷം ", + "years": "{0} വർഷം ", } meridians = { - 'am': 'രാവിലെ', - 'pm': 'ഉച്ചക്ക് ശേഷം', - 'AM': 'രാവിലെ', - 'PM': 'ഉച്ചക്ക് ശേഷം', + "am": "രാവിലെ", + "pm": "ഉച്ചക്ക് ശേഷം", + "AM": "രാവിലെ", + "PM": "ഉച്ചക്ക് ശേഷം", } - month_names = ['', 'ജനുവരി', 'ഫെബ്രുവരി', 'മാർച്ച്‌', 'ഏപ്രിൽ ', 'മെയ്‌ ', 'ജൂണ്‍', 'ജൂലൈ', - 'ഓഗസ്റ്റ്‌', 'സെപ്റ്റംബർ', 'ഒക്ടോബർ', 'നവംബർ', 'ഡിസംബർ'] - month_abbreviations = ['', 'ജനു', 'ഫെബ് ', 'മാർ', 'ഏപ്രിൽ', 'മേയ്', 'ജൂണ്‍', 'ജൂലൈ', 'ഓഗസ്റ', - 'സെപ്റ്റ', 'ഒക്ടോ', 'നവം', 'ഡിസം'] + month_names = [ + "", + "ജനുവരി", + "ഫെബ്രുവരി", + "മാർച്ച്‌", + "ഏപ്രിൽ ", + "മെയ്‌ ", + "ജൂണ്‍", + "ജൂലൈ", + "ഓഗസ്റ്റ്‌", + "സെപ്റ്റംബർ", + "ഒക്ടോബർ", + "നവംബർ", + "ഡിസംബർ", + ] + month_abbreviations = [ + "", + "ജനു", + "ഫെബ് ", + "മാർ", + "ഏപ്രിൽ", + "മേയ്", + "ജൂണ്‍", + "ജൂലൈ", + "ഓഗസ്റ", + "സെപ്റ്റ", + "ഒക്ടോ", + "നവം", + "ഡിസം", + ] - day_names = ['', 'തിങ്കള്‍', 'ചൊവ്വ', 'ബുധന്‍', 'വ്യാഴം', 'വെള്ളി', 'ശനി', 'ഞായര്‍'] - day_abbreviations = ['', 'തിങ്കള്‍', 'ചൊവ്വ', 'ബുധന്‍', 'വ്യാഴം', 'വെള്ളി', 'ശനി', 'ഞായര്‍'] + day_names = ["", "തിങ്കള്‍", "ചൊവ്വ", "ബുധന്‍", "വ്യാഴം", "വെള്ളി", "ശനി", "ഞായര്‍"] + day_abbreviations = [ + "", + "തിങ്കള്‍", + "ചൊവ്വ", + "ബുധന്‍", + "വ്യാഴം", + "വെള്ളി", + "ശനി", + "ഞായര്‍", + ] class HindiLocale(Locale): - names = ['hi'] + names = ["hi"] - past = '{0} पहले' - future = '{0} बाद' + past = "{0} पहले" + future = "{0} बाद" timeframes = { - 'now': 'अभी', - 'seconds': 'सेकंड्', - 'minute': 'एक मिनट ', - 'minutes': '{0} मिनट ', - 'hour': 'एक घंटा', - 'hours': '{0} घंटे', - 'day': 'एक दिन', - 'days': '{0} दिन', - 'month': 'एक माह ', - 'months': '{0} महीने ', - 'year': 'एक वर्ष ', - 'years': '{0} साल ', - } + "now": "अभी", + "seconds": "सेकंड्", + "minute": "एक मिनट ", + "minutes": "{0} मिनट ", + "hour": "एक घंटा", + "hours": "{0} घंटे", + "day": "एक दिन", + "days": "{0} दिन", + "month": "एक माह ", + "months": "{0} महीने ", + "year": "एक वर्ष ", + "years": "{0} साल ", + } + + meridians = {"am": "सुबह", "pm": "शाम", "AM": "सुबह", "PM": "शाम"} - meridians = { - 'am': 'सुबह', - 'pm': 'शाम', - 'AM': 'सुबह', - 'PM': 'शाम', - } + month_names = [ + "", + "जनवरी", + "फरवरी", + "मार्च", + "अप्रैल ", + "मई", + "जून", + "जुलाई", + "अगस्त", + "सितंबर", + "अक्टूबर", + "नवंबर", + "दिसंबर", + ] + month_abbreviations = [ + "", + "जन", + "फ़र", + "मार्च", + "अप्रै", + "मई", + "जून", + "जुलाई", + "आग", + "सित", + "अकत", + "नवे", + "दिस", + ] - month_names = ['', 'जनवरी', 'फरवरी', 'मार्च', 'अप्रैल ', 'मई', 'जून', 'जुलाई', - 'अगस्त', 'सितंबर', 'अक्टूबर', 'नवंबर', 'दिसंबर'] - month_abbreviations = ['', 'जन', 'फ़र', 'मार्च', 'अप्रै', 'मई', 'जून', 'जुलाई', 'आग', - 'सित', 'अकत', 'नवे', 'दिस'] + day_names = [ + "", + "सोमवार", + "मंगलवार", + "बुधवार", + "गुरुवार", + "शुक्रवार", + "शनिवार", + "रविवार", + ] + day_abbreviations = ["", "सोम", "मंगल", "बुध", "गुरुवार", "शुक्र", "शनि", "रवि"] - day_names = ['', 'सोमवार', 'मंगलवार', 'बुधवार', 'गुरुवार', 'शुक्रवार', 'शनिवार', 'रविवार'] - day_abbreviations = ['', 'सोम', 'मंगल', 'बुध', 'गुरुवार', 'शुक्र', 'शनि', 'रवि'] class CzechLocale(Locale): - names = ['cs', 'cs_cz'] + names = ["cs", "cs_cz"] timeframes = { - 'now': 'Teď', - 'seconds': { - 'past': '{0} sekundami', - 'future': ['{0} sekundy', '{0} sekund'] - }, - 'minute': {'past': 'minutou', 'future': 'minutu', 'zero': '{0} minut'}, - 'minutes': { - 'past': '{0} minutami', - 'future': ['{0} minuty', '{0} minut'] - }, - 'hour': {'past': 'hodinou', 'future': 'hodinu', 'zero': '{0} hodin'}, - 'hours': { - 'past': '{0} hodinami', - 'future': ['{0} hodiny', '{0} hodin'] - }, - 'day': {'past': 'dnem', 'future': 'den', 'zero': '{0} dnů'}, - 'days': { - 'past': '{0} dny', - 'future': ['{0} dny', '{0} dnů'] - }, - 'month': {'past': 'měsícem', 'future': 'měsíc', 'zero': '{0} měsíců'}, - 'months': { - 'past': '{0} měsíci', - 'future': ['{0} měsíce', '{0} měsíců'] - }, - 'year': {'past': 'rokem', 'future': 'rok', 'zero': '{0} let'}, - 'years': { - 'past': '{0} lety', - 'future': ['{0} roky', '{0} let'] - } - } - - past = 'Před {0}' - future = 'Za {0}' + "now": "Teď", + "seconds": {"past": "{0} sekundami", "future": ["{0} sekundy", "{0} sekund"]}, + "minute": {"past": "minutou", "future": "minutu", "zero": "{0} minut"}, + "minutes": {"past": "{0} minutami", "future": ["{0} minuty", "{0} minut"]}, + "hour": {"past": "hodinou", "future": "hodinu", "zero": "{0} hodin"}, + "hours": {"past": "{0} hodinami", "future": ["{0} hodiny", "{0} hodin"]}, + "day": {"past": "dnem", "future": "den", "zero": "{0} dnů"}, + "days": {"past": "{0} dny", "future": ["{0} dny", "{0} dnů"]}, + "month": {"past": "měsícem", "future": "měsíc", "zero": "{0} měsíců"}, + "months": {"past": "{0} měsíci", "future": ["{0} měsíce", "{0} měsíců"]}, + "year": {"past": "rokem", "future": "rok", "zero": "{0} let"}, + "years": {"past": "{0} lety", "future": ["{0} roky", "{0} let"]}, + } + + past = "Před {0}" + future = "Za {0}" - month_names = ['', 'leden', 'únor', 'březen', 'duben', 'květen', 'červen', - 'červenec', 'srpen', 'září', 'říjen', 'listopad', 'prosinec'] - month_abbreviations = ['', 'led', 'úno', 'bře', 'dub', 'kvě', 'čvn', 'čvc', - 'srp', 'zář', 'říj', 'lis', 'pro'] - - day_names = ['', 'pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', - 'sobota', 'neděle'] - day_abbreviations = ['', 'po', 'út', 'st', 'čt', 'pá', 'so', 'ne'] + month_names = [ + "", + "leden", + "únor", + "březen", + "duben", + "květen", + "červen", + "červenec", + "srpen", + "září", + "říjen", + "listopad", + "prosinec", + ] + month_abbreviations = [ + "", + "led", + "úno", + "bře", + "dub", + "kvě", + "čvn", + "čvc", + "srp", + "zář", + "říj", + "lis", + "pro", + ] + day_names = [ + "", + "pondělí", + "úterý", + "středa", + "čtvrtek", + "pátek", + "sobota", + "neděle", + ] + day_abbreviations = ["", "po", "út", "st", "čt", "pá", "so", "ne"] def _format_timeframe(self, timeframe, delta): - '''Czech aware time frame format function, takes into account - the differences between past and future forms.''' + """Czech aware time frame format function, takes into account + the differences between past and future forms.""" form = self.timeframes[timeframe] if isinstance(form, dict): if delta == 0: - form = form['zero'] # And *never* use 0 in the singular! + form = form["zero"] # And *never* use 0 in the singular! elif delta > 0: - form = form['future'] + form = form["future"] else: - form = form['past'] + form = form["past"] delta = abs(delta) if isinstance(form, list): @@ -1438,65 +2557,80 @@ def _format_timeframe(self, timeframe, delta): class SlovakLocale(Locale): - names = ['sk', 'sk_sk'] + names = ["sk", "sk_sk"] timeframes = { - 'now': 'Teraz', - 'seconds': { - 'past': 'pár sekundami', - 'future': ['{0} sekundy', '{0} sekúnd'] - }, - 'minute': {'past': 'minútou', 'future': 'minútu', 'zero': '{0} minút'}, - 'minutes': { - 'past': '{0} minútami', - 'future': ['{0} minúty', '{0} minút'] - }, - 'hour': {'past': 'hodinou', 'future': 'hodinu', 'zero': '{0} hodín'}, - 'hours': { - 'past': '{0} hodinami', - 'future': ['{0} hodiny', '{0} hodín'] - }, - 'day': {'past': 'dňom', 'future': 'deň', 'zero': '{0} dní'}, - 'days': { - 'past': '{0} dňami', - 'future': ['{0} dni', '{0} dní'] - }, - 'month': {'past': 'mesiacom', 'future': 'mesiac', 'zero': '{0} mesiacov'}, - 'months': { - 'past': '{0} mesiacmi', - 'future': ['{0} mesiace', '{0} mesiacov'] - }, - 'year': {'past': 'rokom', 'future': 'rok', 'zero': '{0} rokov'}, - 'years': { - 'past': '{0} rokmi', - 'future': ['{0} roky', '{0} rokov'] - } - } + "now": "Teraz", + "seconds": {"past": "pár sekundami", "future": ["{0} sekundy", "{0} sekúnd"]}, + "minute": {"past": "minútou", "future": "minútu", "zero": "{0} minút"}, + "minutes": {"past": "{0} minútami", "future": ["{0} minúty", "{0} minút"]}, + "hour": {"past": "hodinou", "future": "hodinu", "zero": "{0} hodín"}, + "hours": {"past": "{0} hodinami", "future": ["{0} hodiny", "{0} hodín"]}, + "day": {"past": "dňom", "future": "deň", "zero": "{0} dní"}, + "days": {"past": "{0} dňami", "future": ["{0} dni", "{0} dní"]}, + "month": {"past": "mesiacom", "future": "mesiac", "zero": "{0} mesiacov"}, + "months": {"past": "{0} mesiacmi", "future": ["{0} mesiace", "{0} mesiacov"]}, + "year": {"past": "rokom", "future": "rok", "zero": "{0} rokov"}, + "years": {"past": "{0} rokmi", "future": ["{0} roky", "{0} rokov"]}, + } + + past = "Pred {0}" + future = "O {0}" - past = 'Pred {0}' - future = 'O {0}' - - month_names = ['', 'január', 'február', 'marec', 'apríl', 'máj', 'jún', - 'júl', 'august', 'september', 'október', 'november', 'december'] - month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'máj', 'jún', 'júl', - 'aug', 'sep', 'okt', 'nov', 'dec'] - - day_names = ['', 'pondelok', 'utorok', 'streda', 'štvrtok', 'piatok', - 'sobota', 'nedeľa'] - day_abbreviations = ['', 'po', 'ut', 'st', 'št', 'pi', 'so', 'ne'] + month_names = [ + "", + "január", + "február", + "marec", + "apríl", + "máj", + "jún", + "júl", + "august", + "september", + "október", + "november", + "december", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mar", + "apr", + "máj", + "jún", + "júl", + "aug", + "sep", + "okt", + "nov", + "dec", + ] + day_names = [ + "", + "pondelok", + "utorok", + "streda", + "štvrtok", + "piatok", + "sobota", + "nedeľa", + ] + day_abbreviations = ["", "po", "ut", "st", "št", "pi", "so", "ne"] def _format_timeframe(self, timeframe, delta): - '''Slovak aware time frame format function, takes into account - the differences between past and future forms.''' + """Slovak aware time frame format function, takes into account + the differences between past and future forms.""" form = self.timeframes[timeframe] if isinstance(form, dict): if delta == 0: - form = form['zero'] # And *never* use 0 in the singular! + form = form["zero"] # And *never* use 0 in the singular! elif delta > 0: - form = form['future'] + form = form["future"] else: - form = form['past'] + form = form["past"] delta = abs(delta) if isinstance(form, list): @@ -1510,725 +2644,1234 @@ def _format_timeframe(self, timeframe, delta): class FarsiLocale(Locale): - names = ['fa', 'fa_ir'] + names = ["fa", "fa_ir"] - past = '{0} قبل' - future = 'در {0}' + past = "{0} قبل" + future = "در {0}" timeframes = { - 'now': 'اکنون', - 'seconds': 'ثانیه', - 'minute': 'یک دقیقه', - 'minutes': '{0} دقیقه', - 'hour': 'یک ساعت', - 'hours': '{0} ساعت', - 'day': 'یک روز', - 'days': '{0} روز', - 'month': 'یک ماه', - 'months': '{0} ماه', - 'year': 'یک سال', - 'years': '{0} سال', + "now": "اکنون", + "seconds": "ثانیه", + "minute": "یک دقیقه", + "minutes": "{0} دقیقه", + "hour": "یک ساعت", + "hours": "{0} ساعت", + "day": "یک روز", + "days": "{0} روز", + "month": "یک ماه", + "months": "{0} ماه", + "year": "یک سال", + "years": "{0} سال", } meridians = { - 'am': 'قبل از ظهر', - 'pm': 'بعد از ظهر', - 'AM': 'قبل از ظهر', - 'PM': 'بعد از ظهر', + "am": "قبل از ظهر", + "pm": "بعد از ظهر", + "AM": "قبل از ظهر", + "PM": "بعد از ظهر", } - 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'] + 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 = ['', 'دو شنبه', 'سه شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه', 'شنبه', 'یکشنبه'] - day_abbreviations = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + day_names = [ + "", + "دو شنبه", + "سه شنبه", + "چهارشنبه", + "پنجشنبه", + "جمعه", + "شنبه", + "یکشنبه", + ] + day_abbreviations = ["", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] class MacedonianLocale(Locale): - names = ['mk', 'mk_mk'] + names = ["mk", "mk_mk"] - past = 'пред {0}' - future = 'за {0}' + past = "пред {0}" + future = "за {0}" timeframes = { - 'now': 'сега', - 'seconds': 'секунди', - 'minute': 'една минута', - 'minutes': '{0} минути', - 'hour': 'еден саат', - 'hours': '{0} саати', - 'day': 'еден ден', - 'days': '{0} дена', - 'month': 'еден месец', - 'months': '{0} месеци', - 'year': 'една година', - 'years': '{0} години', - } + "now": "сега", + "seconds": "секунди", + "minute": "една минута", + "minutes": "{0} минути", + "hour": "еден саат", + "hours": "{0} саати", + "day": "еден ден", + "days": "{0} дена", + "month": "еден месец", + "months": "{0} месеци", + "year": "една година", + "years": "{0} години", + } + + meridians = {"am": "дп", "pm": "пп", "AM": "претпладне", "PM": "попладне"} - meridians = { - 'am': 'дп', - 'pm': 'пп', - 'AM': 'претпладне', - 'PM': 'попладне', - } - - month_names = ['', 'Јануари', 'Февруари', 'Март', 'Април', 'Мај', 'Јуни', 'Јули', 'Август', 'Септември', 'Октомври', - 'Ноември', 'Декември'] - month_abbreviations = ['', 'Јан.', ' Фев.', ' Мар.', ' Апр.', ' Мај', ' Јун.', ' Јул.', ' Авг.', ' Септ.', ' Окт.', - ' Ноем.', ' Декем.'] + month_names = [ + "", + "Јануари", + "Февруари", + "Март", + "Април", + "Мај", + "Јуни", + "Јули", + "Август", + "Септември", + "Октомври", + "Ноември", + "Декември", + ] + month_abbreviations = [ + "", + "Јан.", + " Фев.", + " Мар.", + " Апр.", + " Мај", + " Јун.", + " Јул.", + " Авг.", + " Септ.", + " Окт.", + " Ноем.", + " Декем.", + ] - day_names = ['', 'Понеделник', ' Вторник', ' Среда', ' Четврток', ' Петок', ' Сабота', ' Недела'] - day_abbreviations = ['', 'Пон.', ' Вт.', ' Сре.', ' Чет.', ' Пет.', ' Саб.', ' Нед.'] + day_names = [ + "", + "Понеделник", + " Вторник", + " Среда", + " Четврток", + " Петок", + " Сабота", + " Недела", + ] + day_abbreviations = [ + "", + "Пон.", + " Вт.", + " Сре.", + " Чет.", + " Пет.", + " Саб.", + " Нед.", + ] class HebrewLocale(Locale): - names = ['he', 'he_IL'] + names = ["he", "he_IL"] - past = 'לפני {0}' - future = 'בעוד {0}' + past = "לפני {0}" + future = "בעוד {0}" timeframes = { - 'now': 'הרגע', - 'seconds': 'שניות', - 'minute': 'דקה', - 'minutes': '{0} דקות', - 'hour': 'שעה', - 'hours': '{0} שעות', - '2-hours': 'שעתיים', - 'day': 'יום', - 'days': '{0} ימים', - '2-days': 'יומיים', - 'month': 'חודש', - 'months': '{0} חודשים', - '2-months': 'חודשיים', - 'year': 'שנה', - 'years': '{0} שנים', - '2-years': 'שנתיים', + "now": "הרגע", + "seconds": "שניות", + "minute": "דקה", + "minutes": "{0} דקות", + "hour": "שעה", + "hours": "{0} שעות", + "2-hours": "שעתיים", + "day": "יום", + "days": "{0} ימים", + "2-days": "יומיים", + "month": "חודש", + "months": "{0} חודשים", + "2-months": "חודשיים", + "year": "שנה", + "years": "{0} שנים", + "2-years": "שנתיים", } meridians = { - 'am': 'לפנ"צ', - 'pm': 'אחר"צ', - 'AM': 'לפני הצהריים', - 'PM': 'אחרי הצהריים', + "am": 'לפנ"צ', + "pm": 'אחר"צ', + "AM": "לפני הצהריים", + "PM": "אחרי הצהריים", } - month_names = ['', 'ינואר', 'פברואר', 'מרץ', 'אפריל', 'מאי', 'יוני', 'יולי', - 'אוגוסט', 'ספטמבר', 'אוקטובר', 'נובמבר', 'דצמבר'] - month_abbreviations = ['', 'ינו׳', 'פבר׳', 'מרץ', 'אפר׳', 'מאי', 'יוני', 'יולי', 'אוג׳', - 'ספט׳', 'אוק׳', 'נוב׳', 'דצמ׳'] + month_names = [ + "", + "ינואר", + "פברואר", + "מרץ", + "אפריל", + "מאי", + "יוני", + "יולי", + "אוגוסט", + "ספטמבר", + "אוקטובר", + "נובמבר", + "דצמבר", + ] + month_abbreviations = [ + "", + "ינו׳", + "פבר׳", + "מרץ", + "אפר׳", + "מאי", + "יוני", + "יולי", + "אוג׳", + "ספט׳", + "אוק׳", + "נוב׳", + "דצמ׳", + ] - day_names = ['', 'שני', 'שלישי', 'רביעי', 'חמישי', 'שישי', 'שבת', 'ראשון'] - day_abbreviations = ['', 'ב׳', 'ג׳', 'ד׳', 'ה׳', 'ו׳', 'ש׳', 'א׳'] + day_names = ["", "שני", "שלישי", "רביעי", "חמישי", "שישי", "שבת", "ראשון"] + day_abbreviations = ["", "ב׳", "ג׳", "ד׳", "ה׳", "ו׳", "ש׳", "א׳"] def _format_timeframe(self, timeframe, delta): - '''Hebrew couple of aware''' - couple = '2-{}'.format(timeframe) + """Hebrew couple of aware""" + couple = "2-{}".format(timeframe) if abs(delta) == 2 and couple in self.timeframes: return self.timeframes[couple].format(abs(delta)) else: return self.timeframes[timeframe].format(abs(delta)) + class MarathiLocale(Locale): - names = ['mr'] + names = ["mr"] - past = '{0} आधी' - future = '{0} नंतर' + past = "{0} आधी" + future = "{0} नंतर" timeframes = { - 'now': 'सद्य', - 'seconds': 'सेकंद', - 'minute': 'एक मिनिट ', - 'minutes': '{0} मिनिट ', - 'hour': 'एक तास', - 'hours': '{0} तास', - 'day': 'एक दिवस', - 'days': '{0} दिवस', - 'month': 'एक महिना ', - 'months': '{0} महिने ', - 'year': 'एक वर्ष ', - 'years': '{0} वर्ष ', - } + "now": "सद्य", + "seconds": "सेकंद", + "minute": "एक मिनिट ", + "minutes": "{0} मिनिट ", + "hour": "एक तास", + "hours": "{0} तास", + "day": "एक दिवस", + "days": "{0} दिवस", + "month": "एक महिना ", + "months": "{0} महिने ", + "year": "एक वर्ष ", + "years": "{0} वर्ष ", + } + + meridians = {"am": "सकाळ", "pm": "संध्याकाळ", "AM": "सकाळ", "PM": "संध्याकाळ"} - meridians = { - 'am': 'सकाळ', - 'pm': 'संध्याकाळ', - 'AM': 'सकाळ', - 'PM': 'संध्याकाळ', - } + month_names = [ + "", + "जानेवारी", + "फेब्रुवारी", + "मार्च", + "एप्रिल", + "मे", + "जून", + "जुलै", + "अॉगस्ट", + "सप्टेंबर", + "अॉक्टोबर", + "नोव्हेंबर", + "डिसेंबर", + ] + month_abbreviations = [ + "", + "जान", + "फेब्रु", + "मार्च", + "एप्रि", + "मे", + "जून", + "जुलै", + "अॉग", + "सप्टें", + "अॉक्टो", + "नोव्हें", + "डिसें", + ] - month_names = ['', 'जानेवारी', 'फेब्रुवारी', 'मार्च', 'एप्रिल', 'मे', 'जून', 'जुलै', - 'अॉगस्ट', 'सप्टेंबर', 'अॉक्टोबर', 'नोव्हेंबर', 'डिसेंबर'] - month_abbreviations = ['', 'जान', 'फेब्रु', 'मार्च', 'एप्रि', 'मे', 'जून', 'जुलै', 'अॉग', - 'सप्टें', 'अॉक्टो', 'नोव्हें', 'डिसें'] + day_names = [ + "", + "सोमवार", + "मंगळवार", + "बुधवार", + "गुरुवार", + "शुक्रवार", + "शनिवार", + "रविवार", + ] + day_abbreviations = ["", "सोम", "मंगळ", "बुध", "गुरु", "शुक्र", "शनि", "रवि"] - day_names = ['', 'सोमवार', 'मंगळवार', 'बुधवार', 'गुरुवार', 'शुक्रवार', 'शनिवार', 'रविवार'] - day_abbreviations = ['', 'सोम', 'मंगळ', 'बुध', 'गुरु', 'शुक्र', 'शनि', 'रवि'] def _map_locales(): locales = {} - for cls_name, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): + for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): if issubclass(cls, Locale): for name in cls.names: locales[name.lower()] = cls return locales + class CatalanLocale(Locale): - names = ['ca', 'ca_es', 'ca_ad', 'ca_fr', 'ca_it'] - past = 'Fa {0}' - future = 'En {0}' + names = ["ca", "ca_es", "ca_ad", "ca_fr", "ca_it"] + past = "Fa {0}" + future = "En {0}" timeframes = { - 'now': 'Ara mateix', - 'seconds': 'segons', - 'minute': '1 minut', - 'minutes': '{0} minuts', - 'hour': 'una hora', - 'hours': '{0} hores', - 'day': 'un dia', - 'days': '{0} dies', - 'month': 'un mes', - 'months': '{0} mesos', - 'year': 'un any', - 'years': '{0} anys', + "now": "Ara mateix", + "seconds": "segons", + "minute": "1 minut", + "minutes": "{0} minuts", + "hour": "una hora", + "hours": "{0} hores", + "day": "un dia", + "days": "{0} dies", + "month": "un mes", + "months": "{0} mesos", + "year": "un any", + "years": "{0} anys", } - month_names = ['', 'Gener', 'Febrer', 'Març', 'Abril', 'Maig', 'Juny', 'Juliol', 'Agost', 'Setembre', 'Octubre', 'Novembre', 'Desembre'] - month_abbreviations = ['', 'Gener', 'Febrer', 'Març', 'Abril', 'Maig', 'Juny', 'Juliol', 'Agost', 'Setembre', 'Octubre', 'Novembre', 'Desembre'] - day_names = ['', 'Dilluns', 'Dimarts', 'Dimecres', 'Dijous', 'Divendres', 'Dissabte', 'Diumenge'] - day_abbreviations = ['', 'Dilluns', 'Dimarts', 'Dimecres', 'Dijous', 'Divendres', 'Dissabte', 'Diumenge'] + month_names = [ + "", + "Gener", + "Febrer", + "Març", + "Abril", + "Maig", + "Juny", + "Juliol", + "Agost", + "Setembre", + "Octubre", + "Novembre", + "Desembre", + ] + month_abbreviations = [ + "", + "Gener", + "Febrer", + "Març", + "Abril", + "Maig", + "Juny", + "Juliol", + "Agost", + "Setembre", + "Octubre", + "Novembre", + "Desembre", + ] + day_names = [ + "", + "Dilluns", + "Dimarts", + "Dimecres", + "Dijous", + "Divendres", + "Dissabte", + "Diumenge", + ] + day_abbreviations = [ + "", + "Dilluns", + "Dimarts", + "Dimecres", + "Dijous", + "Divendres", + "Dissabte", + "Diumenge", + ] + class BasqueLocale(Locale): - names = ['eu', 'eu_eu'] - past = 'duela {0}' - future = '{0}' # I don't know what's the right phrase in Basque for the future. + names = ["eu", "eu_eu"] + past = "duela {0}" + future = "{0}" # I don't know what's the right phrase in Basque for the future. timeframes = { - 'now': 'Orain', - 'seconds': 'segundu', - 'minute': 'minutu bat', - 'minutes': '{0} minutu', - 'hour': 'ordu bat', - 'hours': '{0} ordu', - 'day': 'egun bat', - 'days': '{0} egun', - 'month': 'hilabete bat', - 'months': '{0} hilabet', - 'year': 'urte bat', - 'years': '{0} urte', + "now": "Orain", + "seconds": "segundu", + "minute": "minutu bat", + "minutes": "{0} minutu", + "hour": "ordu bat", + "hours": "{0} ordu", + "day": "egun bat", + "days": "{0} egun", + "month": "hilabete bat", + "months": "{0} hilabet", + "year": "urte bat", + "years": "{0} urte", } - month_names = ['', 'urtarrilak', 'otsailak', 'martxoak', 'apirilak', 'maiatzak', 'ekainak', 'uztailak', 'abuztuak', 'irailak', 'urriak', 'azaroak', 'abenduak'] - month_abbreviations = ['', 'urt', 'ots', 'mar', 'api', 'mai', 'eka', 'uzt', 'abu', 'ira', 'urr', 'aza', 'abe'] - day_names = ['', 'astelehena', 'asteartea', 'asteazkena', 'osteguna', 'ostirala', 'larunbata', 'igandea'] - day_abbreviations = ['', 'al', 'ar', 'az', 'og', 'ol', 'lr', 'ig'] + month_names = [ + "", + "urtarrilak", + "otsailak", + "martxoak", + "apirilak", + "maiatzak", + "ekainak", + "uztailak", + "abuztuak", + "irailak", + "urriak", + "azaroak", + "abenduak", + ] + month_abbreviations = [ + "", + "urt", + "ots", + "mar", + "api", + "mai", + "eka", + "uzt", + "abu", + "ira", + "urr", + "aza", + "abe", + ] + day_names = [ + "", + "astelehena", + "asteartea", + "asteazkena", + "osteguna", + "ostirala", + "larunbata", + "igandea", + ] + day_abbreviations = ["", "al", "ar", "az", "og", "ol", "lr", "ig"] class HungarianLocale(Locale): - names = ['hu', 'hu_hu'] + names = ["hu", "hu_hu"] - past = '{0} ezelőtt' - future = '{0} múlva' + past = "{0} ezelőtt" + future = "{0} múlva" timeframes = { - 'now': 'éppen most', - 'seconds': { - 'past': 'másodpercekkel', - 'future': 'pár másodperc' - }, - 'minute': {'past': 'egy perccel', 'future': 'egy perc'}, - 'minutes': {'past': '{0} perccel', 'future': '{0} perc'}, - 'hour': {'past': 'egy órával', 'future': 'egy óra'}, - 'hours': {'past': '{0} órával', 'future': '{0} óra'}, - 'day': { - 'past': 'egy nappal', - 'future': 'egy nap' - }, - 'days': { - 'past': '{0} nappal', - 'future': '{0} nap' - }, - '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'}, - 'years': {'past': '{0} évvel', 'future': '{0} év'}, + "now": "éppen most", + "seconds": {"past": "másodpercekkel", "future": "pár másodperc"}, + "minute": {"past": "egy perccel", "future": "egy perc"}, + "minutes": {"past": "{0} perccel", "future": "{0} perc"}, + "hour": {"past": "egy órával", "future": "egy óra"}, + "hours": {"past": "{0} órával", "future": "{0} óra"}, + "day": {"past": "egy nappal", "future": "egy nap"}, + "days": {"past": "{0} nappal", "future": "{0} nap"}, + "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"}, + "years": {"past": "{0} évvel", "future": "{0} év"}, } - month_names = ['', 'január', 'február', 'március', 'április', 'május', - 'június', 'július', 'augusztus', 'szeptember', - 'október', 'november', 'december'] - month_abbreviations = ['', 'jan', 'febr', 'márc', 'ápr', 'máj', 'jún', - 'júl', 'aug', 'szept', 'okt', 'nov', 'dec'] + month_names = [ + "", + "január", + "február", + "március", + "április", + "május", + "június", + "július", + "augusztus", + "szeptember", + "október", + "november", + "december", + ] + month_abbreviations = [ + "", + "jan", + "febr", + "márc", + "ápr", + "máj", + "jún", + "júl", + "aug", + "szept", + "okt", + "nov", + "dec", + ] - day_names = ['', 'hétfő', 'kedd', 'szerda', 'csütörtök', 'péntek', - 'szombat', 'vasárnap'] - day_abbreviations = ['', 'hét', 'kedd', 'szer', 'csüt', 'pént', - 'szom', 'vas'] + day_names = [ + "", + "hétfő", + "kedd", + "szerda", + "csütörtök", + "péntek", + "szombat", + "vasárnap", + ] + day_abbreviations = ["", "hét", "kedd", "szer", "csüt", "pént", "szom", "vas"] - meridians = { - 'am': 'de', - 'pm': 'du', - 'AM': 'DE', - 'PM': 'DU', - } + meridians = {"am": "de", "pm": "du", "AM": "DE", "PM": "DU"} def _format_timeframe(self, timeframe, delta): form = self.timeframes[timeframe] if isinstance(form, dict): if delta > 0: - form = form['future'] + form = form["future"] else: - form = form['past'] + form = form["past"] return form.format(abs(delta)) class EsperantoLocale(Locale): - names = ['eo', 'eo_xx'] - past = 'antaŭ {0}' - future = 'post {0}' + names = ["eo", "eo_xx"] + past = "antaŭ {0}" + future = "post {0}" timeframes = { - 'now': 'nun', - 'seconds': 'kelkaj sekundoj', - 'minute': 'unu minuto', - 'minutes': '{0} minutoj', - 'hour': 'un horo', - 'hours': '{0} horoj', - 'day': 'unu tago', - 'days': '{0} tagoj', - 'month': 'unu monato', - 'months': '{0} monatoj', - 'year': 'unu jaro', - 'years': '{0} jaroj', + "now": "nun", + "seconds": "kelkaj sekundoj", + "minute": "unu minuto", + "minutes": "{0} minutoj", + "hour": "un horo", + "hours": "{0} horoj", + "day": "unu tago", + "days": "{0} tagoj", + "month": "unu monato", + "months": "{0} monatoj", + "year": "unu jaro", + "years": "{0} jaroj", } - month_names = ['', 'januaro', 'februaro', 'marto', 'aprilo', 'majo', - 'junio', 'julio', 'aŭgusto', 'septembro', 'oktobro', - 'novembro', 'decembro'] - month_abbreviations = ['', 'jan', 'feb', 'mar', 'apr', 'maj', 'jun', - 'jul', 'aŭg', 'sep', 'okt', 'nov', 'dec'] + month_names = [ + "", + "januaro", + "februaro", + "marto", + "aprilo", + "majo", + "junio", + "julio", + "aŭgusto", + "septembro", + "oktobro", + "novembro", + "decembro", + ] + month_abbreviations = [ + "", + "jan", + "feb", + "mar", + "apr", + "maj", + "jun", + "jul", + "aŭg", + "sep", + "okt", + "nov", + "dec", + ] - day_names = ['', 'lundo', 'mardo', 'merkredo', 'ĵaŭdo', 'vendredo', - 'sabato', 'dimanĉo'] - day_abbreviations = ['', 'lun', 'mar', 'mer', 'ĵaŭ', 'ven', - 'sab', 'dim'] + day_names = [ + "", + "lundo", + "mardo", + "merkredo", + "ĵaŭdo", + "vendredo", + "sabato", + "dimanĉo", + ] + day_abbreviations = ["", "lun", "mar", "mer", "ĵaŭ", "ven", "sab", "dim"] - meridians = { - 'am': 'atm', - 'pm': 'ptm', - 'AM': 'ATM', - 'PM': 'PTM', - } + meridians = {"am": "atm", "pm": "ptm", "AM": "ATM", "PM": "PTM"} - ordinal_day_re = r'((?P[1-3]?[0-9](?=a))a)' + ordinal_day_re = r"((?P[1-3]?[0-9](?=a))a)" def _ordinal_number(self, n): - return '{}a'.format(n) + return "{}a".format(n) class ThaiLocale(Locale): - names = ['th', 'th_th'] + names = ["th", "th_th"] - past = '{0}{1}ที่ผ่านมา' - future = 'ในอีก{1}{0}' + past = "{0}{1}ที่ผ่านมา" + future = "ในอีก{1}{0}" timeframes = { - 'now': 'ขณะนี้', - 'seconds': 'ไม่กี่วินาที', - 'minute': '1 นาที', - 'minutes': '{0} นาที', - 'hour': '1 ชั่วโมง', - 'hours': '{0} ชั่วโมง', - 'day': '1 วัน', - 'days': '{0} วัน', - 'month': '1 เดือน', - 'months': '{0} เดือน', - 'year': '1 ปี', - 'years': '{0} ปี', + "now": "ขณะนี้", + "seconds": "ไม่กี่วินาที", + "minute": "1 นาที", + "minutes": "{0} นาที", + "hour": "1 ชั่วโมง", + "hours": "{0} ชั่วโมง", + "day": "1 วัน", + "days": "{0} วัน", + "month": "1 เดือน", + "months": "{0} เดือน", + "year": "1 ปี", + "years": "{0} ปี", } - month_names = ['', 'มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', - 'พฤษภาคม', 'มิถุนายน', 'กรกฎาคม', 'สิงหาคม', - 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'] - month_abbreviations = ['', 'ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', - 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', - 'พ.ย.', 'ธ.ค.'] + month_names = [ + "", + "มกราคม", + "กุมภาพันธ์", + "มีนาคม", + "เมษายน", + "พฤษภาคม", + "มิถุนายน", + "กรกฎาคม", + "สิงหาคม", + "กันยายน", + "ตุลาคม", + "พฤศจิกายน", + "ธันวาคม", + ] + month_abbreviations = [ + "", + "ม.ค.", + "ก.พ.", + "มี.ค.", + "เม.ย.", + "พ.ค.", + "มิ.ย.", + "ก.ค.", + "ส.ค.", + "ก.ย.", + "ต.ค.", + "พ.ย.", + "ธ.ค.", + ] - day_names = ['', 'จันทร์', 'อังคาร', 'พุธ', 'พฤหัสบดี', 'ศุกร์', - 'เสาร์', 'อาทิตย์'] - day_abbreviations = ['', 'จ', 'อ', 'พ', 'พฤ', 'ศ', 'ส', 'อา'] + day_names = ["", "จันทร์", "อังคาร", "พุธ", "พฤหัสบดี", "ศุกร์", "เสาร์", "อาทิตย์"] + day_abbreviations = ["", "จ", "อ", "พ", "พฤ", "ศ", "ส", "อา"] - meridians = { - 'am': 'am', - 'pm': 'pm', - 'AM': 'AM', - 'PM': 'PM', - } + meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"} BE_OFFSET = 543 def year_full(self, year): - '''Thai always use Buddhist Era (BE) which is CE + 543''' + """Thai always use Buddhist Era (BE) which is CE + 543""" year += self.BE_OFFSET - return '{:04d}'.format(year) + return "{:04d}".format(year) def year_abbreviation(self, year): - '''Thai always use Buddhist Era (BE) which is CE + 543''' + """Thai always use Buddhist Era (BE) which is CE + 543""" year += self.BE_OFFSET - return '{:04d}'.format(year)[2:] + return "{:04d}".format(year)[2:] def _format_relative(self, humanized, timeframe, delta): - '''Thai normally doesn't have any space between words''' - if timeframe == 'now': + """Thai normally doesn't have any space between words""" + if timeframe == "now": return humanized - space = '' if timeframe == 'seconds' else ' ' + space = "" if timeframe == "seconds" else " " direction = self.past if delta < 0 else self.future return direction.format(humanized, space) - class BengaliLocale(Locale): - names = ['bn', 'bn_bd', 'bn_in'] + names = ["bn", "bn_bd", "bn_in"] - past = '{0} আগে' - future = '{0} পরে' + past = "{0} আগে" + future = "{0} পরে" timeframes = { - 'now': 'এখন', - 'seconds': 'সেকেন্ড', - 'minute': 'এক মিনিট', - 'minutes': '{0} মিনিট', - 'hour': 'এক ঘণ্টা', - 'hours': '{0} ঘণ্টা', - 'day': 'এক দিন', - 'days': '{0} দিন', - 'month': 'এক মাস', - 'months': '{0} মাস ', - 'year': 'এক বছর', - 'years': '{0} বছর', - } - - meridians = { - 'am': 'সকাল', - 'pm': 'বিকাল', - 'AM': 'সকাল', - 'PM': 'বিকাল', - } + "now": "এখন", + "seconds": "সেকেন্ড", + "minute": "এক মিনিট", + "minutes": "{0} মিনিট", + "hour": "এক ঘণ্টা", + "hours": "{0} ঘণ্টা", + "day": "এক দিন", + "days": "{0} দিন", + "month": "এক মাস", + "months": "{0} মাস ", + "year": "এক বছর", + "years": "{0} বছর", + } + + meridians = {"am": "সকাল", "pm": "বিকাল", "AM": "সকাল", "PM": "বিকাল"} - month_names = ['', 'জানুয়ারি', 'ফেব্রুয়ারি', 'মার্চ', 'এপ্রিল', 'মে', 'জুন', 'জুলাই', - 'আগস্ট', 'সেপ্টেম্বর', 'অক্টোবর', 'নভেম্বর', 'ডিসেম্বর'] - month_abbreviations = ['', 'জানু', 'ফেব', 'মার্চ', 'এপ্রি', 'মে', 'জুন', 'জুল', - 'অগা','সেপ্ট', 'অক্টো', 'নভে', 'ডিসে'] + month_names = [ + "", + "জানুয়ারি", + "ফেব্রুয়ারি", + "মার্চ", + "এপ্রিল", + "মে", + "জুন", + "জুলাই", + "আগস্ট", + "সেপ্টেম্বর", + "অক্টোবর", + "নভেম্বর", + "ডিসেম্বর", + ] + month_abbreviations = [ + "", + "জানু", + "ফেব", + "মার্চ", + "এপ্রি", + "মে", + "জুন", + "জুল", + "অগা", + "সেপ্ট", + "অক্টো", + "নভে", + "ডিসে", + ] - day_names = ['', 'সোমবার', 'মঙ্গলবার', 'বুধবার', 'বৃহস্পতিবার', 'শুক্রবার', 'শনিবার', 'রবিবার'] - day_abbreviations = ['', 'সোম', 'মঙ্গল', 'বুধ', 'বৃহঃ', 'শুক্র', 'শনি', 'রবি'] + day_names = [ + "", + "সোমবার", + "মঙ্গলবার", + "বুধবার", + "বৃহস্পতিবার", + "শুক্রবার", + "শনিবার", + "রবিবার", + ] + day_abbreviations = ["", "সোম", "মঙ্গল", "বুধ", "বৃহঃ", "শুক্র", "শনি", "রবি"] def _ordinal_number(self, n): if n > 10 or n == 0: - return '{}তম'.format(n) + return "{}তম".format(n) if n in [1, 5, 7, 8, 9, 10]: - return '{}ম'.format(n) + return "{}ম".format(n) if n in [2, 3]: - return '{}য়'.format(n) + return "{}য়".format(n) if n == 4: - return '{}র্থ'.format(n) + return "{}র্থ".format(n) if n == 6: - return '{}ষ্ঠ'.format(n) + return "{}ষ্ঠ".format(n) class RomanshLocale(Locale): - names = ['rm', 'rm_ch'] + names = ["rm", "rm_ch"] - past = 'avant {0}' - future = 'en {0}' + past = "avant {0}" + future = "en {0}" timeframes = { - 'now': 'en quest mument', - 'seconds': 'secundas', - 'minute': 'ina minuta', - 'minutes': '{0} minutas', - 'hour': 'in\'ura', - 'hours': '{0} ura', - 'day': 'in di', - 'days': '{0} dis', - 'month': 'in mais', - 'months': '{0} mais', - 'year': 'in onn', - 'years': '{0} onns', + "now": "en quest mument", + "seconds": "secundas", + "minute": "ina minuta", + "minutes": "{0} minutas", + "hour": "in'ura", + "hours": "{0} ura", + "day": "in di", + "days": "{0} dis", + "month": "in mais", + "months": "{0} mais", + "year": "in onn", + "years": "{0} onns", } month_names = [ - '', 'schaner', 'favrer', 'mars', 'avrigl', 'matg', 'zercladur', - 'fanadur', 'avust', 'settember', 'october', 'november', 'december' + "", + "schaner", + "favrer", + "mars", + "avrigl", + "matg", + "zercladur", + "fanadur", + "avust", + "settember", + "october", + "november", + "december", ] month_abbreviations = [ - '', 'schan', 'fav', 'mars', 'avr', 'matg', 'zer', 'fan', 'avu', - 'set', 'oct', 'nov', 'dec' + "", + "schan", + "fav", + "mars", + "avr", + "matg", + "zer", + "fan", + "avu", + "set", + "oct", + "nov", + "dec", ] day_names = [ - '', 'glindesdi', 'mardi', 'mesemna', 'gievgia', 'venderdi', - 'sonda', 'dumengia' + "", + "glindesdi", + "mardi", + "mesemna", + "gievgia", + "venderdi", + "sonda", + "dumengia", ] - day_abbreviations = [ - '', 'gli', 'ma', 'me', 'gie', 've', 'so', 'du' - ] + day_abbreviations = ["", "gli", "ma", "me", "gie", "ve", "so", "du"] class SwissLocale(Locale): - names = ['de', 'de_ch'] + names = ["de", "de_ch"] - past = 'vor {0}' - future = 'in {0}' + past = "vor {0}" + future = "in {0}" timeframes = { - 'now': 'gerade eben', - 'seconds': 'Sekunden', - 'minute': 'einer Minute', - 'minutes': '{0} Minuten', - 'hour': 'einer Stunde', - 'hours': '{0} Stunden', - 'day': 'einem Tag', - 'days': '{0} Tagen', - 'month': 'einem Monat', - 'months': '{0} Monaten', - 'year': 'einem Jahr', - 'years': '{0} Jahren', - } + "now": "gerade eben", + "seconds": "Sekunden", + "minute": "einer Minute", + "minutes": "{0} Minuten", + "hour": "einer Stunde", + "hours": "{0} Stunden", + "day": "einem Tag", + "days": "{0} Tagen", + "month": "einem Monat", + "months": "{0} Monaten", + "year": "einem Jahr", + "years": "{0} Jahren", + } month_names = [ - '', 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', - 'August', 'September', 'Oktober', 'November', 'Dezember' - ] + "", + "Januar", + "Februar", + "März", + "April", + "Mai", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "Dezember", + ] month_abbreviations = [ - '', 'Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', - 'Okt', 'Nov', 'Dez' - ] + "", + "Jan", + "Feb", + "Mär", + "Apr", + "Mai", + "Jun", + "Jul", + "Aug", + "Sep", + "Okt", + "Nov", + "Dez", + ] day_names = [ - '', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', - 'Samstag', 'Sonntag' - ] + "", + "Montag", + "Dienstag", + "Mittwoch", + "Donnerstag", + "Freitag", + "Samstag", + "Sonntag", + ] - day_abbreviations = [ - '', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So' - ] + day_abbreviations = ["", "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] class RomanianLocale(Locale): - names = ['ro', 'ro_ro'] + names = ["ro", "ro_ro"] - past = '{0} în urmă' - future = 'peste {0}' + past = "{0} în urmă" + future = "peste {0}" timeframes = { - 'now': 'acum', - 'seconds': 'câteva secunde', - 'minute': 'un minut', - 'minutes': '{0} minute', - 'hour': 'o oră', - 'hours': '{0} ore', - 'day': 'o zi', - 'days': '{0} zile', - 'month': 'o lună', - 'months': '{0} luni', - 'year': 'un an', - 'years': '{0} ani', + "now": "acum", + "seconds": "câteva secunde", + "minute": "un minut", + "minutes": "{0} minute", + "hour": "o oră", + "hours": "{0} ore", + "day": "o zi", + "days": "{0} zile", + "month": "o lună", + "months": "{0} luni", + "year": "un an", + "years": "{0} ani", } - month_names = ['', 'ianuarie', 'februarie', 'martie', 'aprilie', 'mai', 'iunie', 'iulie', - 'august', 'septembrie', 'octombrie', 'noiembrie', 'decembrie'] - month_abbreviations = ['', 'ian', 'febr', 'mart', 'apr', 'mai', 'iun', 'iul', 'aug', 'sept', 'oct', 'nov', 'dec'] + month_names = [ + "", + "ianuarie", + "februarie", + "martie", + "aprilie", + "mai", + "iunie", + "iulie", + "august", + "septembrie", + "octombrie", + "noiembrie", + "decembrie", + ] + month_abbreviations = [ + "", + "ian", + "febr", + "mart", + "apr", + "mai", + "iun", + "iul", + "aug", + "sept", + "oct", + "nov", + "dec", + ] - day_names = ['', 'luni', 'marți', 'miercuri', 'joi', 'vineri', 'sâmbătă', 'duminică'] - day_abbreviations = ['', 'Lun', 'Mar', 'Mie', 'Joi', 'Vin', 'Sâm', 'Dum'] + day_names = [ + "", + "luni", + "marți", + "miercuri", + "joi", + "vineri", + "sâmbătă", + "duminică", + ] + day_abbreviations = ["", "Lun", "Mar", "Mie", "Joi", "Vin", "Sâm", "Dum"] class SlovenianLocale(Locale): - names = ['sl', 'sl_si'] + names = ["sl", "sl_si"] - past = 'pred {0}' - future = 'čez {0}' + past = "pred {0}" + future = "čez {0}" timeframes = { - 'now': 'zdaj', - 'seconds': 'sekund', - 'minute': 'minuta', - 'minutes': '{0} minutami', - 'hour': 'uro', - 'hours': '{0} ur', - 'day': 'dan', - 'days': '{0} dni', - 'month': 'mesec', - 'months': '{0} mesecev', - 'year': 'leto', - 'years': '{0} let', - } - - meridians = { - 'am': '', - 'pm': '', - 'AM': '', - 'PM': '', - } + "now": "zdaj", + "seconds": "sekund", + "minute": "minuta", + "minutes": "{0} minutami", + "hour": "uro", + "hours": "{0} ur", + "day": "dan", + "days": "{0} dni", + "month": "mesec", + "months": "{0} mesecev", + "year": "leto", + "years": "{0} let", + } + + meridians = {"am": "", "pm": "", "AM": "", "PM": ""} month_names = [ - '', 'Januar', 'Februar', 'Marec', 'April', 'Maj', 'Junij', 'Julij', - 'Avgust', 'September', 'Oktober', 'November', 'December' + "", + "Januar", + "Februar", + "Marec", + "April", + "Maj", + "Junij", + "Julij", + "Avgust", + "September", + "Oktober", + "November", + "December", ] month_abbreviations = [ - '', 'Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Avg', - 'Sep', 'Okt', 'Nov', 'Dec' + "", + "Jan", + "Feb", + "Mar", + "Apr", + "Maj", + "Jun", + "Jul", + "Avg", + "Sep", + "Okt", + "Nov", + "Dec", ] day_names = [ - '', 'Ponedeljek', 'Torek', 'Sreda', 'Četrtek', 'Petek', 'Sobota', 'Nedelja' + "", + "Ponedeljek", + "Torek", + "Sreda", + "Četrtek", + "Petek", + "Sobota", + "Nedelja", ] - day_abbreviations = [ - '', 'Pon', 'Tor', 'Sre', 'Čet', 'Pet', 'Sob', 'Ned' - ] + day_abbreviations = ["", "Pon", "Tor", "Sre", "Čet", "Pet", "Sob", "Ned"] + class IndonesianLocale(Locale): - names = ['id', 'id_id'] + names = ["id", "id_id"] - past = '{0} yang lalu' - future = 'dalam {0}' + past = "{0} yang lalu" + future = "dalam {0}" timeframes = { - 'now': 'baru saja', - 'seconds': 'detik', - 'minute': '1 menit', - 'minutes': '{0} menit', - 'hour': '1 jam', - 'hours': '{0} jam', - 'day': '1 hari', - 'days': '{0} hari', - 'month': '1 bulan', - 'months': '{0} bulan', - 'year': '1 tahun', - 'years': '{0} tahun' - } - - meridians = { - 'am': '', - 'pm': '', - 'AM': '', - 'PM': '' - } + "now": "baru saja", + "seconds": "detik", + "minute": "1 menit", + "minutes": "{0} menit", + "hour": "1 jam", + "hours": "{0} jam", + "day": "1 hari", + "days": "{0} hari", + "month": "1 bulan", + "months": "{0} bulan", + "year": "1 tahun", + "years": "{0} tahun", + } + + meridians = {"am": "", "pm": "", "AM": "", "PM": ""} - month_names = ['', 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', - 'Agustus', 'September', 'Oktober', 'November', 'Desember'] + month_names = [ + "", + "Januari", + "Februari", + "Maret", + "April", + "Mei", + "Juni", + "Juli", + "Agustus", + "September", + "Oktober", + "November", + "Desember", + ] - month_abbreviations = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', - 'Ags', 'Sept', 'Okt', 'Nov', 'Des'] + month_abbreviations = [ + "", + "Jan", + "Feb", + "Mar", + "Apr", + "Mei", + "Jun", + "Jul", + "Ags", + "Sept", + "Okt", + "Nov", + "Des", + ] - day_names = ['', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'] + day_names = ["", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"] - day_abbreviations = ['', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'] + day_abbreviations = [ + "", + "Senin", + "Selasa", + "Rabu", + "Kamis", + "Jumat", + "Sabtu", + "Minggu", + ] class NepaliLocale(Locale): - names = ['ne', 'ne_np'] + names = ["ne", "ne_np"] - past = '{0} पहिले' - future = '{0} पछी' + past = "{0} पहिले" + future = "{0} पछी" timeframes = { - 'now': 'अहिले', - 'seconds': 'सेकण्ड', - 'minute': 'मिनेट', - 'minutes': '{0} मिनेट', - 'hour': 'एक घण्टा', - 'hours': '{0} घण्टा', - 'day': 'एक दिन', - 'days': '{0} दिन', - 'month': 'एक महिना', - 'months': '{0} महिना', - 'year': 'एक बर्ष', - 'years': 'बर्ष' - } - - meridians = { - 'am': 'पूर्वाह्न', - 'pm': 'अपरान्ह', - 'AM': 'पूर्वाह्न', - 'PM': 'अपरान्ह' - } + "now": "अहिले", + "seconds": "सेकण्ड", + "minute": "मिनेट", + "minutes": "{0} मिनेट", + "hour": "एक घण्टा", + "hours": "{0} घण्टा", + "day": "एक दिन", + "days": "{0} दिन", + "month": "एक महिना", + "months": "{0} महिना", + "year": "एक बर्ष", + "years": "बर्ष", + } + + meridians = {"am": "पूर्वाह्न", "pm": "अपरान्ह", "AM": "पूर्वाह्न", "PM": "अपरान्ह"} month_names = [ - '', 'जनवरी', 'फेब्रुअरी', 'मार्च', 'एप्रील', 'मे', 'जुन', - 'जुलाई', 'अगष्ट', 'सेप्टेम्बर', 'अक्टोबर', 'नोवेम्बर', 'डिसेम्बर' + "", + "जनवरी", + "फेब्रुअरी", + "मार्च", + "एप्रील", + "मे", + "जुन", + "जुलाई", + "अगष्ट", + "सेप्टेम्बर", + "अक्टोबर", + "नोवेम्बर", + "डिसेम्बर", ] month_abbreviations = [ - '', 'जन', 'फेब', 'मार्च', 'एप्रील', 'मे', 'जुन', 'जुलाई', 'अग', - 'सेप', 'अक्ट', 'नोव', 'डिस' + "", + "जन", + "फेब", + "मार्च", + "एप्रील", + "मे", + "जुन", + "जुलाई", + "अग", + "सेप", + "अक्ट", + "नोव", + "डिस", ] day_names = [ - '', 'सोमवार', 'मंगलवार', 'बुधवार', 'बिहिवार', 'शुक्रवार', 'शनिवार', 'आइतवार' + "", + "सोमवार", + "मंगलवार", + "बुधवार", + "बिहिवार", + "शुक्रवार", + "शनिवार", + "आइतवार", ] - day_abbreviations = [ - '', 'सोम', 'मंगल', 'बुध', 'बिहि', 'शुक्र', 'शनि', 'आइत' - ] + day_abbreviations = ["", "सोम", "मंगल", "बुध", "बिहि", "शुक्र", "शनि", "आइत"] + class EstonianLocale(Locale): - names = ['ee', 'et'] + names = ["ee", "et"] - past = '{0} tagasi' - future = '{0} pärast' + past = "{0} tagasi" + future = "{0} pärast" timeframes = { - 'now': {'past': 'just nüüd', 'future': 'just nüüd'}, - 'second': {'past': 'üks sekund', 'future': 'ühe sekundi'}, - 'seconds': {'past': '{0} sekundit', 'future': '{0} sekundi'}, - 'minute': {'past': 'üks minut', 'future': 'ühe minuti'}, - 'minutes': {'past': '{0} minutit', 'future': '{0} minuti'}, - 'hour': {'past': 'tund aega', 'future': 'tunni aja'}, - 'hours': {'past': '{0} tundi', 'future': '{0} tunni'}, - 'day': {'past': 'üks päev', 'future': 'ühe päeva'}, - 'days': {'past': '{0} päeva', 'future': '{0} päeva'}, - 'month': {'past': 'üks kuu', 'future': 'ühe kuu'}, - 'months': {'past': '{0} kuud', 'future': '{0} kuu'}, - 'year': {'past': 'üks aasta', 'future': 'ühe aasta'}, - 'years': {'past': '{0} aastat', 'future': '{0} aasta'} + "now": {"past": "just nüüd", "future": "just nüüd"}, + "second": {"past": "üks sekund", "future": "ühe sekundi"}, + "seconds": {"past": "{0} sekundit", "future": "{0} sekundi"}, + "minute": {"past": "üks minut", "future": "ühe minuti"}, + "minutes": {"past": "{0} minutit", "future": "{0} minuti"}, + "hour": {"past": "tund aega", "future": "tunni aja"}, + "hours": {"past": "{0} tundi", "future": "{0} tunni"}, + "day": {"past": "üks päev", "future": "ühe päeva"}, + "days": {"past": "{0} päeva", "future": "{0} päeva"}, + "month": {"past": "üks kuu", "future": "ühe kuu"}, + "months": {"past": "{0} kuud", "future": "{0} kuu"}, + "year": {"past": "üks aasta", "future": "ühe aasta"}, + "years": {"past": "{0} aastat", "future": "{0} aasta"}, } - month_names = ['', 'Jaanuar', 'Veebruar', 'Märts', 'Aprill', 'Mai', 'Juuni', 'Juuli', - 'August', 'September', 'Oktoober', 'November', 'Detsember'] - month_abbreviations = ['', 'Jan', 'Veb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', - 'Sep', 'Okt', 'Nov', 'Dets'] + month_names = [ + "", + "Jaanuar", + "Veebruar", + "Märts", + "Aprill", + "Mai", + "Juuni", + "Juuli", + "August", + "September", + "Oktoober", + "November", + "Detsember", + ] + month_abbreviations = [ + "", + "Jan", + "Veb", + "Mär", + "Apr", + "Mai", + "Jun", + "Jul", + "Aug", + "Sep", + "Okt", + "Nov", + "Dets", + ] - day_names = ['', 'Esmaspäev', 'Teisipäev', 'Kolmapäev', 'Neljapäev', 'Reede', 'Laupäev', 'Pühapäev'] - day_abbreviations = ['', 'Esm', 'Teis', 'Kolm', 'Nelj', 'Re', 'Lau', 'Püh'] + day_names = [ + "", + "Esmaspäev", + "Teisipäev", + "Kolmapäev", + "Neljapäev", + "Reede", + "Laupäev", + "Pühapäev", + ] + day_abbreviations = ["", "Esm", "Teis", "Kolm", "Nelj", "Re", "Lau", "Püh"] def _format_timeframe(self, timeframe, delta): form = self.timeframes[timeframe] if delta > 0: - form = form['future'] + form = form["future"] else: - form = form['past'] + form = form["past"] return form.format(abs(delta)) + _locales = _map_locales() diff --git a/arrow/parser.py b/arrow/parser.py index b2be6681e..7266ff957 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals +import re from datetime import datetime + from dateutil import tz -import re + +from arrow import locales try: from functools import lru_cache except ImportError: # pragma: no cover from backports.functools_lru_cache import lru_cache # pragma: no cover -from arrow import locales - class ParserError(RuntimeError): pass @@ -20,101 +20,109 @@ class ParserError(RuntimeError): class DateTimeParser(object): - _FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|X)') - _ESCAPE_RE = re.compile(r'\[[^\[\]]*\]') - - _ONE_OR_MORE_DIGIT_RE = re.compile(r'\d+') - _ONE_OR_TWO_DIGIT_RE = re.compile(r'\d{1,2}') - _FOUR_DIGIT_RE = re.compile(r'\d{4}') - _TWO_DIGIT_RE = re.compile(r'\d{2}') - _TZ_RE = re.compile(r'[+\-]?\d{2}:?(\d{2})?') - _TZ_NAME_RE = re.compile(r'\w[\w+\-/]+') + _FORMAT_RE = 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)" + ) + _ESCAPE_RE = re.compile(r"\[[^\[\]]*\]") + _ONE_OR_MORE_DIGIT_RE = re.compile(r"\d+") + _ONE_OR_TWO_DIGIT_RE = re.compile(r"\d{1,2}") + _FOUR_DIGIT_RE = re.compile(r"\d{4}") + _TWO_DIGIT_RE = re.compile(r"\d{2}") + _TZ_RE = re.compile(r"[+\-]?\d{2}:?(\d{2})?") + _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") _BASE_INPUT_RE_MAP = { - 'YYYY': _FOUR_DIGIT_RE, - 'YY': _TWO_DIGIT_RE, - 'MM': _TWO_DIGIT_RE, - 'M': _ONE_OR_TWO_DIGIT_RE, - 'DD': _TWO_DIGIT_RE, - 'D': _ONE_OR_TWO_DIGIT_RE, - 'HH': _TWO_DIGIT_RE, - 'H': _ONE_OR_TWO_DIGIT_RE, - 'hh': _TWO_DIGIT_RE, - 'h': _ONE_OR_TWO_DIGIT_RE, - 'mm': _TWO_DIGIT_RE, - 'm': _ONE_OR_TWO_DIGIT_RE, - 'ss': _TWO_DIGIT_RE, - 's': _ONE_OR_TWO_DIGIT_RE, - 'X': re.compile(r'\d+'), - 'ZZZ': _TZ_NAME_RE, - 'ZZ': _TZ_RE, - 'Z': _TZ_RE, - 'S': _ONE_OR_MORE_DIGIT_RE, + "YYYY": _FOUR_DIGIT_RE, + "YY": _TWO_DIGIT_RE, + "MM": _TWO_DIGIT_RE, + "M": _ONE_OR_TWO_DIGIT_RE, + "DD": _TWO_DIGIT_RE, + "D": _ONE_OR_TWO_DIGIT_RE, + "HH": _TWO_DIGIT_RE, + "H": _ONE_OR_TWO_DIGIT_RE, + "hh": _TWO_DIGIT_RE, + "h": _ONE_OR_TWO_DIGIT_RE, + "mm": _TWO_DIGIT_RE, + "m": _ONE_OR_TWO_DIGIT_RE, + "ss": _TWO_DIGIT_RE, + "s": _ONE_OR_TWO_DIGIT_RE, + "X": re.compile(r"\d+"), + "ZZZ": _TZ_NAME_RE, + "ZZ": _TZ_RE, + "Z": _TZ_RE, + "S": _ONE_OR_MORE_DIGIT_RE, } - MARKERS = ['YYYY', 'MM', 'DD'] - SEPARATORS = ['-', '/', '.'] + MARKERS = ["YYYY", "MM", "DD"] + SEPARATORS = ["-", "/", "."] - def __init__(self, locale='en_us', cache_size=0): + def __init__(self, locale="en_us", cache_size=0): self.locale = locales.get_locale(locale) self._input_re_map = self._BASE_INPUT_RE_MAP.copy() - self._input_re_map.update({ - 'MMMM': self._choice_re(self.locale.month_names[1:], re.IGNORECASE), - 'MMM': self._choice_re(self.locale.month_abbreviations[1:], - re.IGNORECASE), - 'Do': re.compile(self.locale.ordinal_day_re), - 'dddd': self._choice_re(self.locale.day_names[1:], re.IGNORECASE), - 'ddd': self._choice_re(self.locale.day_abbreviations[1:], - re.IGNORECASE), - 'd': re.compile(r"[1-7]"), - 'a': self._choice_re( - (self.locale.meridians['am'], self.locale.meridians['pm']) - ), - # note: 'A' token accepts both 'am/pm' and 'AM/PM' formats to - # ensure backwards compatibility of this token - 'A': self._choice_re(self.locale.meridians.values()) - }) + self._input_re_map.update( + { + "MMMM": self._choice_re(self.locale.month_names[1:], re.IGNORECASE), + "MMM": self._choice_re( + self.locale.month_abbreviations[1:], re.IGNORECASE + ), + "Do": re.compile(self.locale.ordinal_day_re), + "dddd": self._choice_re(self.locale.day_names[1:], re.IGNORECASE), + "ddd": self._choice_re( + self.locale.day_abbreviations[1:], re.IGNORECASE + ), + "d": re.compile(r"[1-7]"), + "a": self._choice_re( + (self.locale.meridians["am"], self.locale.meridians["pm"]) + ), + # note: 'A' token accepts both 'am/pm' and 'AM/PM' formats to + # ensure backwards compatibility of this token + "A": self._choice_re(self.locale.meridians.values()), + } + ) if cache_size > 0: - self._generate_pattern_re =\ - lru_cache(maxsize=cache_size)(self._generate_pattern_re) + self._generate_pattern_re = lru_cache(maxsize=cache_size)( + self._generate_pattern_re + ) def parse_iso(self, string): - has_time = 'T' in string or ' ' in string.strip() - space_divider = ' ' in string.strip() + has_time = "T" in string or " " in string.strip() + space_divider = " " in string.strip() if has_time: if space_divider: - date_string, time_string = string.split(' ', 1) + date_string, time_string = string.split(" ", 1) else: - date_string, time_string = string.split('T', 1) - time_parts = re.split('[+-]', time_string, 1) + date_string, time_string = string.split("T", 1) + time_parts = re.split("[+-]", time_string, 1) has_tz = len(time_parts) > 1 - has_seconds = time_parts[0].count(':') > 1 - has_subseconds = re.search('[.,]', time_parts[0]) + has_seconds = time_parts[0].count(":") > 1 + has_subseconds = re.search("[.,]", time_parts[0]) if has_subseconds: - formats = ['YYYY-MM-DDTHH:mm:ss%sS' % has_subseconds.group()] + formats = ["YYYY-MM-DDTHH:mm:ss%sS" % has_subseconds.group()] elif has_seconds: - formats = ['YYYY-MM-DDTHH:mm:ss'] + formats = ["YYYY-MM-DDTHH:mm:ss"] else: - formats = ['YYYY-MM-DDTHH:mm'] + formats = ["YYYY-MM-DDTHH:mm"] else: has_tz = False # generate required formats: YYYY-MM-DD, YYYY-MM-DD, YYYY # using various separators: -, /, . - l = len(self.MARKERS) - formats = [separator.join(self.MARKERS[:l-i]) - for i in range(l) - for separator in self.SEPARATORS] + len_markers = len(self.MARKERS) + formats = [ + separator.join(self.MARKERS[: len_markers - i]) + for i in range(len_markers) + for separator in self.SEPARATORS + ] if has_time and has_tz: - formats = [f + 'Z' for f in formats] + formats = [f + "Z" for f in formats] if space_divider: - formats = [item.replace('T', ' ', 1) for item in formats] + formats = [item.replace("T", " ", 1) for item in formats] return self._parse_multiformat(string, formats) @@ -133,7 +141,7 @@ def _generate_pattern_re(self, fmt): # Extract the bracketed expressions to be reinserted later. escaped_fmt = re.sub(self._ESCAPE_RE, "#", escaped_fmt) # Any number of S is the same as one. - escaped_fmt = re.sub('S+', 'S', escaped_fmt) + escaped_fmt = re.sub("S+", "S", escaped_fmt) escaped_data = re.findall(self._ESCAPE_RE, fmt) fmt_pattern = escaped_fmt @@ -143,14 +151,18 @@ def _generate_pattern_re(self, fmt): try: input_re = self._input_re_map[token] except KeyError: - raise ParserError('Unrecognized token \'{}\''.format(token)) - input_pattern = '(?P<{}>{})'.format(token, input_re.pattern) + raise ParserError("Unrecognized token '{}'".format(token)) + input_pattern = "(?P<{}>{})".format(token, input_re.pattern) tokens.append(token) # a pattern doesn't have the same length as the token # it replaces! We keep the difference in the offset variable. # This works because the string is scanned left-to-right and matches # are returned in the order found by finditer. - fmt_pattern = fmt_pattern[:m.start() + offset] + input_pattern + fmt_pattern[m.end() + offset:] + fmt_pattern = ( + fmt_pattern[: m.start() + offset] + + input_pattern + + fmt_pattern[m.end() + offset :] + ) offset += len(input_pattern) - (m.end() - m.start()) final_fmt_pattern = "" @@ -174,12 +186,15 @@ def parse(self, string, fmt): match = fmt_pattern_re.search(string) if match is None: - raise ParserError('Failed to match \'{}\' when parsing \'{}\'' - .format(fmt_pattern_re.pattern, string)) + raise ParserError( + "Failed to match '{}' when parsing '{}'".format( + fmt_pattern_re.pattern, string + ) + ) parts = {} for token in fmt_tokens: - if token == 'Do': - value = match.group('value') + if token == "Do": + value = match.group("value") else: value = match.group(token) self._parse_token(token, value, parts) @@ -187,38 +202,38 @@ def parse(self, string, fmt): def _parse_token(self, token, value, parts): - if token == 'YYYY': - parts['year'] = int(value) - elif token == 'YY': + if token == "YYYY": + parts["year"] = int(value) + elif token == "YY": value = int(value) - parts['year'] = 1900 + value if value > 68 else 2000 + value + parts["year"] = 1900 + value if value > 68 else 2000 + value - elif token in ['MMMM', 'MMM']: - parts['month'] = self.locale.month_number(value.lower()) + elif token in ["MMMM", "MMM"]: + parts["month"] = self.locale.month_number(value.lower()) - elif token in ['MM', 'M']: - parts['month'] = int(value) + elif token in ["MM", "M"]: + parts["month"] = int(value) - elif token in ['DD', 'D']: - parts['day'] = int(value) + elif token in ["DD", "D"]: + parts["day"] = int(value) - elif token in ['Do']: - parts['day'] = int(value) + elif token in ["Do"]: + parts["day"] = int(value) - elif token.upper() in ['HH', 'H']: - parts['hour'] = int(value) + elif token.upper() in ["HH", "H"]: + parts["hour"] = int(value) - elif token in ['mm', 'm']: - parts['minute'] = int(value) + elif token in ["mm", "m"]: + parts["minute"] = int(value) - elif token in ['ss', 's']: - parts['second'] = int(value) + elif token in ["ss", "s"]: + parts["second"] = int(value) - elif token == 'S': + elif token == "S": # We have the *most significant* digits of an arbitrary-precision integer. # We want the six most significant digits as an integer, rounded. # FIXME: add nanosecond support somehow? - value = value.ljust(7, str('0')) + value = value.ljust(7, str("0")) # floating-point (IEEE-754) defaults to half-to-even rounding seventh_digit = int(value[6]) @@ -229,47 +244,47 @@ def _parse_token(self, token, value, parts): else: rounding = 0 - parts['microsecond'] = int(value[:6]) + rounding + parts["microsecond"] = int(value[:6]) + rounding - elif token == 'X': - parts['timestamp'] = int(value) + elif token == "X": + parts["timestamp"] = int(value) - elif token in ['ZZZ', 'ZZ', 'Z']: - parts['tzinfo'] = TzinfoParser.parse(value) + elif token in ["ZZZ", "ZZ", "Z"]: + parts["tzinfo"] = TzinfoParser.parse(value) - elif token in ['a', 'A']: - if value in ( - self.locale.meridians['am'], - self.locale.meridians['AM'] - ): - parts['am_pm'] = 'am' - elif value in ( - self.locale.meridians['pm'], - self.locale.meridians['PM'] - ): - parts['am_pm'] = 'pm' + elif token in ["a", "A"]: + if value in (self.locale.meridians["am"], self.locale.meridians["AM"]): + parts["am_pm"] = "am" + elif value in (self.locale.meridians["pm"], self.locale.meridians["PM"]): + parts["am_pm"] = "pm" @staticmethod def _build_datetime(parts): - timestamp = parts.get('timestamp') + timestamp = parts.get("timestamp") if timestamp: tz_utc = tz.tzutc() return datetime.fromtimestamp(timestamp, tz=tz_utc) - am_pm = parts.get('am_pm') - hour = parts.get('hour', 0) + am_pm = parts.get("am_pm") + hour = parts.get("hour", 0) - if am_pm == 'pm' and hour < 12: + if am_pm == "pm" and hour < 12: hour += 12 - elif am_pm == 'am' and hour == 12: + elif am_pm == "am" and hour == 12: hour = 0 - return datetime(year=parts.get('year', 1), month=parts.get('month', 1), - day=parts.get('day', 1), hour=hour, minute=parts.get('minute', 0), - second=parts.get('second', 0), microsecond=parts.get('microsecond', 0), - tzinfo=parts.get('tzinfo')) + return datetime( + year=parts.get("year", 1), + month=parts.get("month", 1), + day=parts.get("day", 1), + hour=hour, + minute=parts.get("minute", 0), + second=parts.get("second", 0), + microsecond=parts.get("microsecond", 0), + tzinfo=parts.get("tzinfo"), + ) def _parse_multiformat(self, string, formats): @@ -283,7 +298,9 @@ def _parse_multiformat(self, string, formats): pass if _datetime is None: - raise ParserError('Could not match input to any of {} on \'{}\''.format(formats, string)) + raise ParserError( + "Could not match input to any of {} on '{}'".format(formats, string) + ) return _datetime @@ -300,27 +317,27 @@ def _try_timestamp(string): try: return float(string) - except: + except Exception: return None @staticmethod def _choice_re(choices, flags=0): - return re.compile('({})'.format('|'.join(choices)), flags=flags) + return re.compile(r"({})".format("|".join(choices)), flags=flags) class TzinfoParser(object): - _TZINFO_RE = re.compile(r'([+\-])?(\d\d):?(\d\d)?') + _TZINFO_RE = re.compile(r"([+\-])?(\d\d):?(\d\d)?") @classmethod def parse(cls, string): tzinfo = None - if string == 'local': + if string == "local": tzinfo = tz.tzlocal() - elif string in ['utc', 'UTC']: + elif string in ["utc", "UTC"]: tzinfo = tz.tzutc() else: @@ -333,7 +350,7 @@ def parse(cls, string): minutes = 0 seconds = int(hours) * 3600 + int(minutes) * 60 - if sign == '-': + if sign == "-": seconds *= -1 tzinfo = tz.tzoffset(None, seconds) diff --git a/arrow/util.py b/arrow/util.py index 5358ef13a..03132f7ed 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -import functools import sys import warnings + def total_seconds(td): # pragma: no cover return td.total_seconds() @@ -15,7 +15,7 @@ def is_timestamp(value): try: float(value) return True - except: + except Exception: return False @@ -25,7 +25,8 @@ def is_timestamp(value): basestring def isstr(s): - return isinstance(s, basestring) + return isinstance(s, basestring) # noqa: F821 + except NameError: # pragma: no cover @@ -34,14 +35,17 @@ def isstr(s): class list_to_iter_shim(list): - ''' A temporary shim for functions that currently return a list but that will, after a + """ A temporary shim for functions that currently return a list but that will, after a deprecation period, return an iteratator. - ''' + """ def __init__(self, iterable=(), **kwargs): - ''' Equivalent to list(iterable). warn_text will be emitted on all non-iterator operations. - ''' - self._warn_text = kwargs.pop('warn_text', None) or 'this object will be converted to an iterator in a future release' + """ Equivalent to list(iterable). warn_text will be emitted on all non-iterator operations. + """ + self._warn_text = ( + kwargs.pop("warn_text", None) + or "this object will be converted to an iterator in a future release" + ) self._iter_count = 0 list.__init__(self, iterable, **kwargs) @@ -56,34 +60,36 @@ def __iter__(self): def _wrap_method(name): list_func = getattr(list, name) + def wrapper(self, *args, **kwargs): self._warn() return list_func(self, *args, **kwargs) + return wrapper - __contains__ = _wrap_method('__contains__') - __add__ = _wrap_method('__add__') - __mul__ = _wrap_method('__mul__') - __getitem__ = _wrap_method('__getitem__') + __contains__ = _wrap_method("__contains__") + __add__ = _wrap_method("__add__") + __mul__ = _wrap_method("__mul__") + __getitem__ = _wrap_method("__getitem__") # Ideally, we would throw warnings from __len__, but list(x) calls len(x) - index = _wrap_method('index') - count = _wrap_method('count') - __setitem__ = _wrap_method('__setitem__') - __delitem__ = _wrap_method('__delitem__') - append = _wrap_method('append') + index = _wrap_method("index") + count = _wrap_method("count") + __setitem__ = _wrap_method("__setitem__") + __delitem__ = _wrap_method("__delitem__") + append = _wrap_method("append") if sys.version_info.major >= 3: # pragma: no cover - clear = _wrap_method('clear') - copy = _wrap_method('copy') - extend = _wrap_method('extend') - __iadd__ = _wrap_method('__iadd__') - __imul__ = _wrap_method('__imul__') - insert = _wrap_method('insert') - pop = _wrap_method('pop') - remove = _wrap_method('remove') - reverse = _wrap_method('reverse') - sort = _wrap_method('sort') + clear = _wrap_method("clear") + copy = _wrap_method("copy") + extend = _wrap_method("extend") + __iadd__ = _wrap_method("__iadd__") + __imul__ = _wrap_method("__imul__") + insert = _wrap_method("insert") + pop = _wrap_method("pop") + remove = _wrap_method("remove") + reverse = _wrap_method("reverse") + sort = _wrap_method("sort") del _wrap_method -__all__ = ['total_seconds', 'is_timestamp', 'isstr', 'list_to_iter_shim', 'list_to_iter_deprecation'] +__all__ = ["total_seconds", "is_timestamp", "isstr", "list_to_iter_shim"] diff --git a/docs/Makefile b/docs/Makefile index 298ea9e21..51285967a 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -16,4 +16,4 @@ help: # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_themes/COPYING.txt b/docs/_themes/COPYING.txt index 206caf8f6..069f469b8 100644 --- a/docs/_themes/COPYING.txt +++ b/docs/_themes/COPYING.txt @@ -12,4 +12,3 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see - diff --git a/docs/_themes/f6/layout.html b/docs/_themes/f6/layout.html index e14c64618..426be8542 100644 --- a/docs/_themes/f6/layout.html +++ b/docs/_themes/f6/layout.html @@ -11,7 +11,7 @@ {# put the sidebar before the body #} {% block sidebarlogo %} -Fork me on GitHub

github.com/crsmithdev/arrow

diff --git a/docs/conf.py b/docs/conf.py index 6a3d18e59..827d143a6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full @@ -12,45 +13,50 @@ import os import sys -sys.path.insert(0, os.path.abspath('..')) + +sys.path.insert(0, os.path.abspath("..")) # -- Project information ----------------------------------------------------- -project = 'arrow' -copyright = '2019, Chris Smith' -author = 'Chris Smith' +project = "arrow" +copyright = "2019, Chris Smith" +author = "Chris Smith" # The full version, including alpha/beta/rc tags -release = '0.13.2' +release = "0.13.2" # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ - 'sphinx.ext.autodoc' -] +extensions = ["sphinx.ext.autodoc"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '_themes/README.rst', '_themes/f6/README.rst'] +exclude_patterns = [ + "_build", + "Thumbs.db", + ".DS_Store", + "_themes/README.rst", + "_themes/f6/README.rst", +] -master_doc = 'index' -source_suffix = '.rst' -pygments_style = 'sphinx' +master_doc = "index" +source_suffix = ".rst" +pygments_style = "sphinx" # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'f6' -html_theme_path = ['_themes'] +html_theme = "f6" +html_theme_path = ["_themes"] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/index.rst b/docs/index.rst index 8e2f1d8fe..87d422599 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -287,7 +287,7 @@ Or another Arrow, or datetime: >>> future.humanize(present) 'in 2 hours' -Support for a growing number of locales (see `locales.py` for supported languages): +Support for a growing number of locales (see ``locales.py`` for supported languages): .. code-block:: python diff --git a/requirements.txt b/requirements.txt index cf9186581..d5ca714e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ -python-dateutil==2.8.0 -pytz==2019.1 +backports.functools_lru_cache==1.5.0 +chai==1.1.2 nose==1.3.7 nose-cov==1.6 -chai==1.1.2 +pre-commit==1.16.1 +python-dateutil==2.8.0 +pytz==2019.1 simplejson==3.16.0 sphinx==1.8.5; python_version == '2.7' sphinx==2.0.1; python_version >= '3.5' -backports.functools_lru_cache==1.5.0 diff --git a/setup.cfg b/setup.cfg index c4d614dc1..e08567216 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,28 @@ [nosetests] where = tests verbosity = 2 - all-modules = true with-coverage = true cover-min-percentage = 100 cover-package = arrow cover-erase = true +[coverage:run] +branch = True +source = + arrow + tests + +[coverage:report] +show_missing = True +fail_under = 100 + +[flake8] +per-file-ignores = arrow/__init__.py:F401 +ignore = E203,E501,W503 + +[tool:isort] +known_third_party = chai,dateutil,pytz,setuptools,simplejson + [bdist_wheel] -universal=1 +universal = 1 diff --git a/setup.py b/setup.py index 18a7b1a6e..4a2febcb0 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from setuptools import setup from arrow import __version__ @@ -38,6 +39,6 @@ keywords="arrow date time datetime", project_urls={ "Repository": "https://github.com/crsmithdev/arrow", - "Bug Reports": "https://github.com/crsmithdev/arrow/issues" + "Bug Reports": "https://github.com/crsmithdev/arrow/issues", }, ) diff --git a/tests/api_tests.py b/tests/api_tests.py index f8959291a..1cf171653 100644 --- a/tests/api_tests.py +++ b/tests/api_tests.py @@ -1,38 +1,33 @@ +# -*- coding: utf-8 -*- from chai import Chai -from datetime import datetime -from dateutil import tz -import time -from arrow import api, factory, arrow, util +from arrow import api, arrow, factory class ModuleTests(Chai): - def test_get(self): - expect(api._factory.get).args(1, b=2).returns('result') + self.expect(api._factory.get).args(1, b=2).returns("result") - assertEqual(api.get(1, b=2), 'result') + self.assertEqual(api.get(1, b=2), "result") def test_utcnow(self): - expect(api._factory.utcnow).returns('utcnow') + self.expect(api._factory.utcnow).returns("utcnow") - assertEqual(api.utcnow(), 'utcnow') + self.assertEqual(api.utcnow(), "utcnow") def test_now(self): - expect(api._factory.now).args('tz').returns('now') + self.expect(api._factory.now).args("tz").returns("now") - assertEqual(api.now('tz'), 'now') + self.assertEqual(api.now("tz"), "now") def test_factory(self): - class MockCustomArrowClass(arrow.Arrow): pass result = api.factory(MockCustomArrowClass) - assertIsInstance(result, factory.ArrowFactory) - assertIsInstance(result.utcnow(), MockCustomArrowClass) - + self.assertIsInstance(result, factory.ArrowFactory) + self.assertIsInstance(result.utcnow(), MockCustomArrowClass) diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 851f88939..825db674c 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1,39 +1,36 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from chai import Chai - -from datetime import date, datetime, timedelta -from dateutil import tz -from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU -import simplejson as json -import warnings import calendar import pickle -import time import sys +import time +import warnings +from datetime import date, datetime, timedelta + +import simplejson as json +from chai import Chai +from dateutil import tz +from dateutil.relativedelta import FR, MO, SA, SU, TH, TU, WE from arrow import arrow, util def assertDtEqual(dt1, dt2, within=10): - assertEqual(dt1.tzinfo, dt2.tzinfo) - assertTrue(abs(util.total_seconds(dt1 - dt2)) < within) + assertEqual(dt1.tzinfo, dt2.tzinfo) # noqa: F821 + assertTrue(abs(util.total_seconds(dt1 - dt2)) < within) # noqa: F821 class ArrowInitTests(Chai): - def test_init(self): result = arrow.Arrow(2013, 2, 2, 12, 30, 45, 999999) - expected = datetime(2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.tzutc()) + self.expected = datetime(2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.tzutc()) - assertEqual(result._datetime, expected) + self.assertEqual(result._datetime, self.expected) class ArrowFactoryTests(Chai): - def test_now(self): result = arrow.Arrow.now() @@ -60,43 +57,46 @@ def test_fromdatetime(self): result = arrow.Arrow.fromdatetime(dt) - assertEqual(result._datetime, dt.replace(tzinfo=tz.tzutc())) + self.assertEqual(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=tz.gettz("US/Pacific")) result = arrow.Arrow.fromdatetime(dt) - assertEqual(result._datetime, dt.replace(tzinfo=tz.gettz('US/Pacific'))) + self.assertEqual(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')) + result = arrow.Arrow.fromdatetime(dt, tz.gettz("US/Pacific")) - assertEqual(result._datetime, dt.replace(tzinfo=tz.gettz('US/Pacific'))) + self.assertEqual(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')) + result = arrow.Arrow.fromdate(dt, tz.gettz("US/Pacific")) - assertEqual(result._datetime, datetime(2013, 2, 3, tzinfo=tz.gettz('US/Pacific'))) + self.assertEqual( + 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') + 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') + result = arrow.Arrow.strptime(formatted, "%Y-%m-%d %H:%M:%S") - assertEqual(result._datetime, datetime(2013, 2, 3, 12, 30, 45, tzinfo=tz.tzutc())) + self.assertEqual( + result._datetime, datetime(2013, 2, 3, 12, 30, 45, tzinfo=tz.tzutc()) + ) class ArrowRepresentationTests(Chai): - def setUp(self): super(ArrowRepresentationTests, self).setUp() @@ -106,48 +106,49 @@ def test_repr(self): result = self.arrow.__repr__() - assertEqual(result, ''.format(self.arrow._datetime.isoformat())) + self.assertEqual( + result, "".format(self.arrow._datetime.isoformat()) + ) def test_str(self): result = self.arrow.__str__() - assertEqual(result, self.arrow._datetime.isoformat()) + self.assertEqual(result, self.arrow._datetime.isoformat()) def test_hash(self): result = self.arrow.__hash__() - assertEqual(result, self.arrow._datetime.__hash__()) + self.assertEqual(result, self.arrow._datetime.__hash__()) def test_format(self): - result = '{:YYYY-MM-DD}'.format(self.arrow) + result = "{:YYYY-MM-DD}".format(self.arrow) - assertEqual(result, '2013-02-03') + self.assertEqual(result, "2013-02-03") def test_bare_format(self): result = self.arrow.format() - assertEqual(result, '2013-02-03 12:30:45+00:00') + self.assertEqual(result, "2013-02-03 12:30:45+00:00") def test_format_no_format_string(self): - result = '{}'.format(self.arrow) + result = "{}".format(self.arrow) - assertEqual(result, str(self.arrow)) + self.assertEqual(result, str(self.arrow)) def test_clone(self): result = self.arrow.clone() - assertTrue(result is not self.arrow) - assertEqual(result._datetime, self.arrow._datetime) + self.assertTrue(result is not self.arrow) + self.assertEqual(result._datetime, self.arrow._datetime) class ArrowAttributeTests(Chai): - def setUp(self): super(ArrowAttributeTests, self).setUp() @@ -155,12 +156,12 @@ def setUp(self): def test_getattr_base(self): - with assertRaises(AttributeError): + with self.assertRaises(AttributeError): self.arrow.prop def test_getattr_week(self): - assertEqual(self.arrow.week, 1) + self.assertEqual(self.arrow.week, 1) def test_getattr_quarter(self): # start dates @@ -168,47 +169,48 @@ def test_getattr_quarter(self): q2 = arrow.Arrow(2013, 4, 1) q3 = arrow.Arrow(2013, 8, 1) q4 = arrow.Arrow(2013, 10, 1) - assertEqual(q1.quarter, 1) - assertEqual(q2.quarter, 2) - assertEqual(q3.quarter, 3) - assertEqual(q4.quarter, 4) + self.assertEqual(q1.quarter, 1) + self.assertEqual(q2.quarter, 2) + self.assertEqual(q3.quarter, 3) + self.assertEqual(q4.quarter, 4) # end dates q1 = arrow.Arrow(2013, 3, 31) q2 = arrow.Arrow(2013, 6, 30) q3 = arrow.Arrow(2013, 9, 30) q4 = arrow.Arrow(2013, 12, 31) - assertEqual(q1.quarter, 1) - assertEqual(q2.quarter, 2) - assertEqual(q3.quarter, 3) - assertEqual(q4.quarter, 4) + self.assertEqual(q1.quarter, 1) + self.assertEqual(q2.quarter, 2) + self.assertEqual(q3.quarter, 3) + self.assertEqual(q4.quarter, 4) def test_getattr_dt_value(self): - assertEqual(self.arrow.year, 2013) + self.assertEqual(self.arrow.year, 2013) def test_tzinfo(self): - self.arrow.tzinfo = tz.gettz('PST') - assertEqual(self.arrow.tzinfo, tz.gettz('PST')) + self.arrow.tzinfo = tz.gettz("PST") + self.assertEqual(self.arrow.tzinfo, tz.gettz("PST")) def test_naive(self): - assertEqual(self.arrow.naive, self.arrow._datetime.replace(tzinfo=None)) + self.assertEqual(self.arrow.naive, self.arrow._datetime.replace(tzinfo=None)) def test_timestamp(self): - assertEqual(self.arrow.timestamp, calendar.timegm(self.arrow._datetime.utctimetuple())) + self.assertEqual( + self.arrow.timestamp, calendar.timegm(self.arrow._datetime.utctimetuple()) + ) def test_float_timestamp(self): result = self.arrow.float_timestamp - self.arrow.timestamp - assertEqual(result, self.arrow.microsecond) + self.assertEqual(result, self.arrow.microsecond) class ArrowComparisonTests(Chai): - def setUp(self): super(ArrowComparisonTests, self).setUp() @@ -216,15 +218,15 @@ def setUp(self): def test_eq(self): - assertTrue(self.arrow == self.arrow) - assertTrue(self.arrow == self.arrow.datetime) - assertFalse(self.arrow == 'abc') + self.assertTrue(self.arrow == self.arrow) + self.assertTrue(self.arrow == self.arrow.datetime) + self.assertFalse(self.arrow == "abc") def test_ne(self): - assertFalse(self.arrow != self.arrow) - assertFalse(self.arrow != self.arrow.datetime) - assertTrue(self.arrow != 'abc') + self.assertFalse(self.arrow != self.arrow) + self.assertFalse(self.arrow != self.arrow.datetime) + self.assertTrue(self.arrow != "abc") def test_deprecated_replace(self): @@ -252,47 +254,46 @@ def test_gt(self): arrow_cmp = self.arrow.shift(minutes=1) - assertFalse(self.arrow > self.arrow) - assertFalse(self.arrow > self.arrow.datetime) + self.assertFalse(self.arrow > self.arrow) + self.assertFalse(self.arrow > self.arrow.datetime) - with assertRaises(TypeError): - self.arrow > 'abc' + with self.assertRaises(TypeError): + self.arrow > "abc" - assertTrue(self.arrow < arrow_cmp) - assertTrue(self.arrow < arrow_cmp.datetime) + self.assertTrue(self.arrow < arrow_cmp) + self.assertTrue(self.arrow < arrow_cmp.datetime) def test_ge(self): - with assertRaises(TypeError): - self.arrow >= 'abc' + with self.assertRaises(TypeError): + self.arrow >= "abc" - assertTrue(self.arrow >= self.arrow) - assertTrue(self.arrow >= self.arrow.datetime) + self.assertTrue(self.arrow >= self.arrow) + self.assertTrue(self.arrow >= self.arrow.datetime) def test_lt(self): arrow_cmp = self.arrow.shift(minutes=1) - assertFalse(self.arrow < self.arrow) - assertFalse(self.arrow < self.arrow.datetime) + self.assertFalse(self.arrow < self.arrow) + self.assertFalse(self.arrow < self.arrow.datetime) - with assertRaises(TypeError): - self.arrow < 'abc' + with self.assertRaises(TypeError): + self.arrow < "abc" - assertTrue(self.arrow < arrow_cmp) - assertTrue(self.arrow < arrow_cmp.datetime) + self.assertTrue(self.arrow < arrow_cmp) + self.assertTrue(self.arrow < arrow_cmp.datetime) def test_le(self): - with assertRaises(TypeError): - self.arrow <= 'abc' + with self.assertRaises(TypeError): + self.arrow <= "abc" - assertTrue(self.arrow <= self.arrow) - assertTrue(self.arrow <= self.arrow.datetime) + self.assertTrue(self.arrow <= self.arrow) + self.assertTrue(self.arrow <= self.arrow.datetime) class ArrowMathTests(Chai): - def setUp(self): super(ArrowMathTests, self).setUp() @@ -302,56 +303,55 @@ def test_add_timedelta(self): result = self.arrow.__add__(timedelta(days=1)) - assertEqual(result._datetime, datetime(2013, 1, 2, tzinfo=tz.tzutc())) + self.assertEqual(result._datetime, datetime(2013, 1, 2, tzinfo=tz.tzutc())) def test_add_other(self): - with assertRaises(TypeError): + with self.assertRaises(TypeError): self.arrow + 1 def test_radd(self): result = self.arrow.__radd__(timedelta(days=1)) - assertEqual(result._datetime, datetime(2013, 1, 2, tzinfo=tz.tzutc())) + self.assertEqual(result._datetime, datetime(2013, 1, 2, tzinfo=tz.tzutc())) def test_sub_timedelta(self): result = self.arrow.__sub__(timedelta(days=1)) - assertEqual(result._datetime, datetime(2012, 12, 31, tzinfo=tz.tzutc())) + self.assertEqual(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())) - assertEqual(result, timedelta(days=11)) + self.assertEqual(result, timedelta(days=11)) def test_sub_arrow(self): result = self.arrow.__sub__(arrow.Arrow(2012, 12, 21, tzinfo=tz.tzutc())) - assertEqual(result, timedelta(days=11)) + self.assertEqual(result, timedelta(days=11)) def test_sub_other(self): - with assertRaises(TypeError): + with self.assertRaises(TypeError): self.arrow - object() def test_rsub_datetime(self): result = self.arrow.__rsub__(datetime(2012, 12, 21, tzinfo=tz.tzutc())) - assertEqual(result, timedelta(days=-11)) + self.assertEqual(result, timedelta(days=-11)) def test_rsub_other(self): - with assertRaises(TypeError): + with self.assertRaises(TypeError): timedelta(days=1) - self.arrow class ArrowDatetimeInterfaceTests(Chai): - def setUp(self): super(ArrowDatetimeInterfaceTests, self).setUp() @@ -361,116 +361,116 @@ def test_date(self): result = self.arrow.date() - assertEqual(result, self.arrow._datetime.date()) + self.assertEqual(result, self.arrow._datetime.date()) def test_time(self): result = self.arrow.time() - assertEqual(result, self.arrow._datetime.time()) + self.assertEqual(result, self.arrow._datetime.time()) def test_timetz(self): result = self.arrow.timetz() - assertEqual(result, self.arrow._datetime.timetz()) + self.assertEqual(result, self.arrow._datetime.timetz()) def test_astimezone(self): - other_tz = tz.gettz('US/Pacific') + other_tz = tz.gettz("US/Pacific") result = self.arrow.astimezone(other_tz) - assertEqual(result, self.arrow._datetime.astimezone(other_tz)) + self.assertEqual(result, self.arrow._datetime.astimezone(other_tz)) def test_utcoffset(self): result = self.arrow.utcoffset() - assertEqual(result, self.arrow._datetime.utcoffset()) + self.assertEqual(result, self.arrow._datetime.utcoffset()) def test_dst(self): result = self.arrow.dst() - assertEqual(result, self.arrow._datetime.dst()) + self.assertEqual(result, self.arrow._datetime.dst()) def test_timetuple(self): result = self.arrow.timetuple() - assertEqual(result, self.arrow._datetime.timetuple()) + self.assertEqual(result, self.arrow._datetime.timetuple()) def test_utctimetuple(self): result = self.arrow.utctimetuple() - assertEqual(result, self.arrow._datetime.utctimetuple()) + self.assertEqual(result, self.arrow._datetime.utctimetuple()) def test_toordinal(self): result = self.arrow.toordinal() - assertEqual(result, self.arrow._datetime.toordinal()) + self.assertEqual(result, self.arrow._datetime.toordinal()) def test_weekday(self): result = self.arrow.weekday() - assertEqual(result, self.arrow._datetime.weekday()) + self.assertEqual(result, self.arrow._datetime.weekday()) def test_isoweekday(self): result = self.arrow.isoweekday() - assertEqual(result, self.arrow._datetime.isoweekday()) + self.assertEqual(result, self.arrow._datetime.isoweekday()) def test_isocalendar(self): result = self.arrow.isocalendar() - assertEqual(result, self.arrow._datetime.isocalendar()) + self.assertEqual(result, self.arrow._datetime.isocalendar()) def test_isoformat(self): result = self.arrow.isoformat() - assertEqual(result, self.arrow._datetime.isoformat()) + self.assertEqual(result, self.arrow._datetime.isoformat()) def test_simplejson(self): - result = json.dumps({'v': self.arrow.for_json()}, for_json=True) + result = json.dumps({"v": self.arrow.for_json()}, for_json=True) - assertEqual(json.loads(result)['v'], self.arrow._datetime.isoformat()) + self.assertEqual(json.loads(result)["v"], self.arrow._datetime.isoformat()) def test_ctime(self): result = self.arrow.ctime() - assertEqual(result, self.arrow._datetime.ctime()) + self.assertEqual(result, self.arrow._datetime.ctime()) def test_strftime(self): - result = self.arrow.strftime('%Y') + result = self.arrow.strftime("%Y") - assertEqual(result, self.arrow._datetime.strftime('%Y')) + self.assertEqual(result, self.arrow._datetime.strftime("%Y")) class ArrowConversionTests(Chai): - 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, tz.gettz("US/Pacific")) - expected = dt_from.replace(tzinfo=tz.gettz('US/Pacific')).astimezone(tz.tzutc()) + self.expected = dt_from.replace(tzinfo=tz.gettz("US/Pacific")).astimezone( + tz.tzutc() + ) - assertEqual(arrow_from.to('UTC').datetime, expected) - assertEqual(arrow_from.to(tz.tzutc()).datetime, expected) + self.assertEqual(arrow_from.to("UTC").datetime, self.expected) + self.assertEqual(arrow_from.to(tz.tzutc()).datetime, self.expected) class ArrowPicklingTests(Chai): - def test_pickle_and_unpickle(self): dt = arrow.Arrow.utcnow() @@ -479,58 +479,65 @@ def test_pickle_and_unpickle(self): unpickled = pickle.loads(pickled) - assertEqual(unpickled, dt) + self.assertEqual(unpickled, dt) class ArrowReplaceTests(Chai): - def test_not_attr(self): - with assertRaises(AttributeError): + with self.assertRaises(AttributeError): arrow.Arrow.utcnow().replace(abc=1) def test_replace(self): arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) - assertEqual(arw.replace(year=2012), arrow.Arrow(2012, 5, 5, 12, 30, 45)) - assertEqual(arw.replace(month=1), arrow.Arrow(2013, 1, 5, 12, 30, 45)) - assertEqual(arw.replace(day=1), arrow.Arrow(2013, 5, 1, 12, 30, 45)) - assertEqual(arw.replace(hour=1), arrow.Arrow(2013, 5, 5, 1, 30, 45)) - assertEqual(arw.replace(minute=1), arrow.Arrow(2013, 5, 5, 12, 1, 45)) - assertEqual(arw.replace(second=1), arrow.Arrow(2013, 5, 5, 12, 30, 1)) + self.assertEqual(arw.replace(year=2012), arrow.Arrow(2012, 5, 5, 12, 30, 45)) + self.assertEqual(arw.replace(month=1), arrow.Arrow(2013, 1, 5, 12, 30, 45)) + self.assertEqual(arw.replace(day=1), arrow.Arrow(2013, 5, 1, 12, 30, 45)) + self.assertEqual(arw.replace(hour=1), arrow.Arrow(2013, 5, 5, 1, 30, 45)) + self.assertEqual(arw.replace(minute=1), arrow.Arrow(2013, 5, 5, 12, 1, 45)) + self.assertEqual(arw.replace(second=1), arrow.Arrow(2013, 5, 5, 12, 30, 1)) def test_replace_shift(self): arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) # This is all scheduled for deprecation - assertEqual(arw.replace(years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) - assertEqual(arw.replace(quarters=1), arrow.Arrow(2013, 8, 5, 12, 30, 45)) - assertEqual(arw.replace(quarters=1, months=1), arrow.Arrow(2013, 9, 5, 12, 30, 45)) - assertEqual(arw.replace(months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) - assertEqual(arw.replace(weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) - assertEqual(arw.replace(days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - assertEqual(arw.replace(hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) - assertEqual(arw.replace(minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) - assertEqual(arw.replace(seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) - assertEqual(arw.replace(microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1)) + self.assertEqual(arw.replace(years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) + self.assertEqual(arw.replace(quarters=1), arrow.Arrow(2013, 8, 5, 12, 30, 45)) + self.assertEqual( + arw.replace(quarters=1, months=1), arrow.Arrow(2013, 9, 5, 12, 30, 45) + ) + self.assertEqual(arw.replace(months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) + self.assertEqual(arw.replace(weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) + self.assertEqual(arw.replace(days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + self.assertEqual(arw.replace(hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) + self.assertEqual(arw.replace(minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) + self.assertEqual(arw.replace(seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) + self.assertEqual( + arw.replace(microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1) + ) def test_replace_shift_negative(self): arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) # This is all scheduled for deprecation - assertEqual(arw.replace(years=-1), arrow.Arrow(2012, 5, 5, 12, 30, 45)) - assertEqual(arw.replace(quarters=-1), arrow.Arrow(2013, 2, 5, 12, 30, 45)) - assertEqual(arw.replace(quarters=-1, months=-1), arrow.Arrow(2013, 1, 5, 12, 30, 45)) - assertEqual(arw.replace(months=-1), arrow.Arrow(2013, 4, 5, 12, 30, 45)) - assertEqual(arw.replace(weeks=-1), arrow.Arrow(2013, 4, 28, 12, 30, 45)) - assertEqual(arw.replace(days=-1), arrow.Arrow(2013, 5, 4, 12, 30, 45)) - assertEqual(arw.replace(hours=-1), arrow.Arrow(2013, 5, 5, 11, 30, 45)) - assertEqual(arw.replace(minutes=-1), arrow.Arrow(2013, 5, 5, 12, 29, 45)) - assertEqual(arw.replace(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) - assertEqual(arw.replace(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999)) + self.assertEqual(arw.replace(years=-1), arrow.Arrow(2012, 5, 5, 12, 30, 45)) + self.assertEqual(arw.replace(quarters=-1), arrow.Arrow(2013, 2, 5, 12, 30, 45)) + self.assertEqual( + arw.replace(quarters=-1, months=-1), arrow.Arrow(2013, 1, 5, 12, 30, 45) + ) + self.assertEqual(arw.replace(months=-1), arrow.Arrow(2013, 4, 5, 12, 30, 45)) + self.assertEqual(arw.replace(weeks=-1), arrow.Arrow(2013, 4, 28, 12, 30, 45)) + self.assertEqual(arw.replace(days=-1), arrow.Arrow(2013, 5, 4, 12, 30, 45)) + self.assertEqual(arw.replace(hours=-1), arrow.Arrow(2013, 5, 5, 11, 30, 45)) + self.assertEqual(arw.replace(minutes=-1), arrow.Arrow(2013, 5, 5, 12, 29, 45)) + self.assertEqual(arw.replace(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) + self.assertEqual( + arw.replace(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999) + ) def test_replace_quarters_bug(self): @@ -539,137 +546,168 @@ def test_replace_quarters_bug(self): # The value of the last-read argument was used instead of the ``quarters`` argument. # Recall that the keyword argument dict, like all dicts, is unordered, so only certain # combinations of arguments would exhibit this. - assertEqual(arw.replace(quarters=0, years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) - assertEqual(arw.replace(quarters=0, months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) - assertEqual(arw.replace(quarters=0, weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) - assertEqual(arw.replace(quarters=0, days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - assertEqual(arw.replace(quarters=0, hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) - assertEqual(arw.replace(quarters=0, minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) - assertEqual(arw.replace(quarters=0, seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) - assertEqual(arw.replace(quarters=0, microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1)) + self.assertEqual( + arw.replace(quarters=0, years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45) + ) + self.assertEqual( + arw.replace(quarters=0, months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45) + ) + self.assertEqual( + arw.replace(quarters=0, weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45) + ) + self.assertEqual( + arw.replace(quarters=0, days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45) + ) + self.assertEqual( + arw.replace(quarters=0, hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45) + ) + self.assertEqual( + arw.replace(quarters=0, minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45) + ) + self.assertEqual( + arw.replace(quarters=0, seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46) + ) + self.assertEqual( + arw.replace(quarters=0, microseconds=1), + arrow.Arrow(2013, 5, 5, 12, 30, 45, 1), + ) def test_replace_tzinfo(self): - arw = arrow.Arrow.utcnow().to('US/Eastern') + arw = arrow.Arrow.utcnow().to("US/Eastern") - result = arw.replace(tzinfo=tz.gettz('US/Pacific')) + result = arw.replace(tzinfo=tz.gettz("US/Pacific")) - assertEqual(result, arw.datetime.replace(tzinfo=tz.gettz('US/Pacific'))) + self.assertEqual(result, arw.datetime.replace(tzinfo=tz.gettz("US/Pacific"))) def test_replace_week(self): - with assertRaises(AttributeError): + with self.assertRaises(AttributeError): arrow.Arrow.utcnow().replace(week=1) def test_replace_quarter(self): - with assertRaises(AttributeError): + with self.assertRaises(AttributeError): arrow.Arrow.utcnow().replace(quarter=1) def test_replace_other_kwargs(self): - with assertRaises(AttributeError): - arrow.utcnow().replace(abc='def') + with self.assertRaises(AttributeError): + arrow.utcnow().replace(abc="def") -class ArrowShiftTests(Chai): +class ArrowShiftTests(Chai): def test_not_attr(self): - with assertRaises(AttributeError): + with self.assertRaises(AttributeError): arrow.Arrow.utcnow().shift(abc=1) def test_shift(self): arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) - assertEqual(arw.shift(years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) - assertEqual(arw.shift(quarters=1), arrow.Arrow(2013, 8, 5, 12, 30, 45)) - assertEqual(arw.shift(quarters=1, months=1), arrow.Arrow(2013, 9, 5, 12, 30, 45)) - assertEqual(arw.shift(months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) - assertEqual(arw.shift(weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) - assertEqual(arw.shift(days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - assertEqual(arw.shift(hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) - assertEqual(arw.shift(minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) - assertEqual(arw.shift(seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) - assertEqual(arw.shift(microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1)) + self.assertEqual(arw.shift(years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) + self.assertEqual(arw.shift(quarters=1), arrow.Arrow(2013, 8, 5, 12, 30, 45)) + self.assertEqual( + arw.shift(quarters=1, months=1), arrow.Arrow(2013, 9, 5, 12, 30, 45) + ) + self.assertEqual(arw.shift(months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) + self.assertEqual(arw.shift(weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) + self.assertEqual(arw.shift(days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + self.assertEqual(arw.shift(hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) + self.assertEqual(arw.shift(minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) + self.assertEqual(arw.shift(seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) + self.assertEqual( + arw.shift(microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1) + ) # Remember: Python's weekday 0 is Monday - assertEqual(arw.shift(weekday=0), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - assertEqual(arw.shift(weekday=1), arrow.Arrow(2013, 5, 7, 12, 30, 45)) - assertEqual(arw.shift(weekday=2), arrow.Arrow(2013, 5, 8, 12, 30, 45)) - assertEqual(arw.shift(weekday=3), arrow.Arrow(2013, 5, 9, 12, 30, 45)) - assertEqual(arw.shift(weekday=4), arrow.Arrow(2013, 5, 10, 12, 30, 45)) - assertEqual(arw.shift(weekday=5), arrow.Arrow(2013, 5, 11, 12, 30, 45)) - assertEqual(arw.shift(weekday=6), arw) - - with assertRaises(IndexError): + self.assertEqual(arw.shift(weekday=0), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=1), arrow.Arrow(2013, 5, 7, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=2), arrow.Arrow(2013, 5, 8, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=3), arrow.Arrow(2013, 5, 9, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=4), arrow.Arrow(2013, 5, 10, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=5), arrow.Arrow(2013, 5, 11, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=6), arw) + + with self.assertRaises(IndexError): arw.shift(weekday=7) # Use dateutil.relativedelta's convenient day instances - assertEqual(arw.shift(weekday=MO), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - assertEqual(arw.shift(weekday=MO(0)), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - assertEqual(arw.shift(weekday=MO(1)), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - assertEqual(arw.shift(weekday=MO(2)), arrow.Arrow(2013, 5, 13, 12, 30, 45)) - assertEqual(arw.shift(weekday=TU), arrow.Arrow(2013, 5, 7, 12, 30, 45)) - assertEqual(arw.shift(weekday=TU(0)), arrow.Arrow(2013, 5, 7, 12, 30, 45)) - assertEqual(arw.shift(weekday=TU(1)), arrow.Arrow(2013, 5, 7, 12, 30, 45)) - assertEqual(arw.shift(weekday=TU(2)), arrow.Arrow(2013, 5, 14, 12, 30, 45)) - assertEqual(arw.shift(weekday=WE), arrow.Arrow(2013, 5, 8, 12, 30, 45)) - assertEqual(arw.shift(weekday=WE(0)), arrow.Arrow(2013, 5, 8, 12, 30, 45)) - assertEqual(arw.shift(weekday=WE(1)), arrow.Arrow(2013, 5, 8, 12, 30, 45)) - assertEqual(arw.shift(weekday=WE(2)), arrow.Arrow(2013, 5, 15, 12, 30, 45)) - assertEqual(arw.shift(weekday=TH), arrow.Arrow(2013, 5, 9, 12, 30, 45)) - assertEqual(arw.shift(weekday=TH(0)), arrow.Arrow(2013, 5, 9, 12, 30, 45)) - assertEqual(arw.shift(weekday=TH(1)), arrow.Arrow(2013, 5, 9, 12, 30, 45)) - assertEqual(arw.shift(weekday=TH(2)), arrow.Arrow(2013, 5, 16, 12, 30, 45)) - assertEqual(arw.shift(weekday=FR), arrow.Arrow(2013, 5, 10, 12, 30, 45)) - assertEqual(arw.shift(weekday=FR(0)), arrow.Arrow(2013, 5, 10, 12, 30, 45)) - assertEqual(arw.shift(weekday=FR(1)), arrow.Arrow(2013, 5, 10, 12, 30, 45)) - assertEqual(arw.shift(weekday=FR(2)), arrow.Arrow(2013, 5, 17, 12, 30, 45)) - assertEqual(arw.shift(weekday=SA), arrow.Arrow(2013, 5, 11, 12, 30, 45)) - assertEqual(arw.shift(weekday=SA(0)), arrow.Arrow(2013, 5, 11, 12, 30, 45)) - assertEqual(arw.shift(weekday=SA(1)), arrow.Arrow(2013, 5, 11, 12, 30, 45)) - assertEqual(arw.shift(weekday=SA(2)), arrow.Arrow(2013, 5, 18, 12, 30, 45)) - assertEqual(arw.shift(weekday=SU), arw) - assertEqual(arw.shift(weekday=SU(0)), arw) - assertEqual(arw.shift(weekday=SU(1)), arw) - assertEqual(arw.shift(weekday=SU(2)), arrow.Arrow(2013, 5, 12, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=MO), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=MO(0)), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=MO(1)), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=MO(2)), arrow.Arrow(2013, 5, 13, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TU), arrow.Arrow(2013, 5, 7, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TU(0)), arrow.Arrow(2013, 5, 7, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TU(1)), arrow.Arrow(2013, 5, 7, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TU(2)), arrow.Arrow(2013, 5, 14, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=WE), arrow.Arrow(2013, 5, 8, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=WE(0)), arrow.Arrow(2013, 5, 8, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=WE(1)), arrow.Arrow(2013, 5, 8, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=WE(2)), arrow.Arrow(2013, 5, 15, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TH), arrow.Arrow(2013, 5, 9, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TH(0)), arrow.Arrow(2013, 5, 9, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TH(1)), arrow.Arrow(2013, 5, 9, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TH(2)), arrow.Arrow(2013, 5, 16, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=FR), arrow.Arrow(2013, 5, 10, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=FR(0)), arrow.Arrow(2013, 5, 10, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=FR(1)), arrow.Arrow(2013, 5, 10, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=FR(2)), arrow.Arrow(2013, 5, 17, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=SA), arrow.Arrow(2013, 5, 11, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=SA(0)), arrow.Arrow(2013, 5, 11, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=SA(1)), arrow.Arrow(2013, 5, 11, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=SA(2)), arrow.Arrow(2013, 5, 18, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=SU), arw) + self.assertEqual(arw.shift(weekday=SU(0)), arw) + self.assertEqual(arw.shift(weekday=SU(1)), arw) + self.assertEqual(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) - assertEqual(arw.shift(years=-1), arrow.Arrow(2012, 5, 5, 12, 30, 45)) - assertEqual(arw.shift(quarters=-1), arrow.Arrow(2013, 2, 5, 12, 30, 45)) - assertEqual(arw.shift(quarters=-1, months=-1), arrow.Arrow(2013, 1, 5, 12, 30, 45)) - assertEqual(arw.shift(months=-1), arrow.Arrow(2013, 4, 5, 12, 30, 45)) - assertEqual(arw.shift(weeks=-1), arrow.Arrow(2013, 4, 28, 12, 30, 45)) - assertEqual(arw.shift(days=-1), arrow.Arrow(2013, 5, 4, 12, 30, 45)) - assertEqual(arw.shift(hours=-1), arrow.Arrow(2013, 5, 5, 11, 30, 45)) - assertEqual(arw.shift(minutes=-1), arrow.Arrow(2013, 5, 5, 12, 29, 45)) - assertEqual(arw.shift(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) - assertEqual(arw.shift(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999)) + self.assertEqual(arw.shift(years=-1), arrow.Arrow(2012, 5, 5, 12, 30, 45)) + self.assertEqual(arw.shift(quarters=-1), arrow.Arrow(2013, 2, 5, 12, 30, 45)) + self.assertEqual( + arw.shift(quarters=-1, months=-1), arrow.Arrow(2013, 1, 5, 12, 30, 45) + ) + self.assertEqual(arw.shift(months=-1), arrow.Arrow(2013, 4, 5, 12, 30, 45)) + self.assertEqual(arw.shift(weeks=-1), arrow.Arrow(2013, 4, 28, 12, 30, 45)) + self.assertEqual(arw.shift(days=-1), arrow.Arrow(2013, 5, 4, 12, 30, 45)) + self.assertEqual(arw.shift(hours=-1), arrow.Arrow(2013, 5, 5, 11, 30, 45)) + self.assertEqual(arw.shift(minutes=-1), arrow.Arrow(2013, 5, 5, 12, 29, 45)) + self.assertEqual(arw.shift(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) + self.assertEqual( + arw.shift(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999) + ) # Not sure how practical these negative weekdays are - assertEqual(arw.shift(weekday=-1), arw.shift(weekday=SU)) - assertEqual(arw.shift(weekday=-2), arw.shift(weekday=SA)) - assertEqual(arw.shift(weekday=-3), arw.shift(weekday=FR)) - assertEqual(arw.shift(weekday=-4), arw.shift(weekday=TH)) - assertEqual(arw.shift(weekday=-5), arw.shift(weekday=WE)) - assertEqual(arw.shift(weekday=-6), arw.shift(weekday=TU)) - assertEqual(arw.shift(weekday=-7), arw.shift(weekday=MO)) - - with assertRaises(IndexError): + self.assertEqual(arw.shift(weekday=-1), arw.shift(weekday=SU)) + self.assertEqual(arw.shift(weekday=-2), arw.shift(weekday=SA)) + self.assertEqual(arw.shift(weekday=-3), arw.shift(weekday=FR)) + self.assertEqual(arw.shift(weekday=-4), arw.shift(weekday=TH)) + self.assertEqual(arw.shift(weekday=-5), arw.shift(weekday=WE)) + self.assertEqual(arw.shift(weekday=-6), arw.shift(weekday=TU)) + self.assertEqual(arw.shift(weekday=-7), arw.shift(weekday=MO)) + + with self.assertRaises(IndexError): arw.shift(weekday=-8) - assertEqual(arw.shift(weekday=MO(-1)), arrow.Arrow(2013, 4, 29, 12, 30, 45)) - assertEqual(arw.shift(weekday=TU(-1)), arrow.Arrow(2013, 4, 30, 12, 30, 45)) - assertEqual(arw.shift(weekday=WE(-1)), arrow.Arrow(2013, 5, 1, 12, 30, 45)) - assertEqual(arw.shift(weekday=TH(-1)), arrow.Arrow(2013, 5, 2, 12, 30, 45)) - assertEqual(arw.shift(weekday=FR(-1)), arrow.Arrow(2013, 5, 3, 12, 30, 45)) - assertEqual(arw.shift(weekday=SA(-1)), arrow.Arrow(2013, 5, 4, 12, 30, 45)) - assertEqual(arw.shift(weekday=SU(-1)), arw) - assertEqual(arw.shift(weekday=SU(-2)), arrow.Arrow(2013, 4, 28, 12, 30, 45)) + self.assertEqual( + arw.shift(weekday=MO(-1)), arrow.Arrow(2013, 4, 29, 12, 30, 45) + ) + self.assertEqual( + arw.shift(weekday=TU(-1)), arrow.Arrow(2013, 4, 30, 12, 30, 45) + ) + self.assertEqual(arw.shift(weekday=WE(-1)), arrow.Arrow(2013, 5, 1, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TH(-1)), arrow.Arrow(2013, 5, 2, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=FR(-1)), arrow.Arrow(2013, 5, 3, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=SA(-1)), arrow.Arrow(2013, 5, 4, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=SU(-1)), arw) + self.assertEqual( + arw.shift(weekday=SU(-2)), arrow.Arrow(2013, 4, 28, 12, 30, 45) + ) def test_shift_quarters_bug(self): @@ -678,335 +716,557 @@ def test_shift_quarters_bug(self): # The value of the last-read argument was used instead of the ``quarters`` argument. # Recall that the keyword argument dict, like all dicts, is unordered, so only certain # combinations of arguments would exhibit this. - assertEqual(arw.replace(quarters=0, years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) - assertEqual(arw.replace(quarters=0, months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) - assertEqual(arw.replace(quarters=0, weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) - assertEqual(arw.replace(quarters=0, days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - assertEqual(arw.replace(quarters=0, hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) - assertEqual(arw.replace(quarters=0, minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) - assertEqual(arw.replace(quarters=0, seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) - assertEqual(arw.replace(quarters=0, microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1)) + self.assertEqual( + arw.replace(quarters=0, years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45) + ) + self.assertEqual( + arw.replace(quarters=0, months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45) + ) + self.assertEqual( + arw.replace(quarters=0, weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45) + ) + self.assertEqual( + arw.replace(quarters=0, days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45) + ) + self.assertEqual( + arw.replace(quarters=0, hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45) + ) + self.assertEqual( + arw.replace(quarters=0, minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45) + ) + self.assertEqual( + arw.replace(quarters=0, seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46) + ) + self.assertEqual( + arw.replace(quarters=0, microseconds=1), + arrow.Arrow(2013, 5, 5, 12, 30, 45, 1), + ) -class ArrowRangeTests(Chai): +class ArrowRangeTests(Chai): def test_year(self): - result = list(arrow.Arrow.range('year', datetime(2013, 1, 2, 3, 4, 5), - datetime(2016, 4, 5, 6, 7, 8))) - - assertEqual(result, [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2014, 1, 2, 3, 4, 5), - arrow.Arrow(2015, 1, 2, 3, 4, 5), - arrow.Arrow(2016, 1, 2, 3, 4, 5), - ]) + result = list( + arrow.Arrow.range( + "year", datetime(2013, 1, 2, 3, 4, 5), datetime(2016, 4, 5, 6, 7, 8) + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2014, 1, 2, 3, 4, 5), + arrow.Arrow(2015, 1, 2, 3, 4, 5), + arrow.Arrow(2016, 1, 2, 3, 4, 5), + ], + ) def test_quarter(self): - result = list(arrow.Arrow.range('quarter', datetime(2013, 2, 3, 4, 5, 6), - datetime(2013, 5, 6, 7, 8, 9))) + result = list( + arrow.Arrow.range( + "quarter", datetime(2013, 2, 3, 4, 5, 6), datetime(2013, 5, 6, 7, 8, 9) + ) + ) - assertEqual(result, [ - arrow.Arrow(2013, 2, 3, 4, 5, 6), - arrow.Arrow(2013, 5, 3, 4, 5, 6), - ]) + self.assertEqual( + result, [arrow.Arrow(2013, 2, 3, 4, 5, 6), arrow.Arrow(2013, 5, 3, 4, 5, 6)] + ) def test_month(self): - result = list(arrow.Arrow.range('month', datetime(2013, 2, 3, 4, 5, 6), - datetime(2013, 5, 6, 7, 8, 9))) - - assertEqual(result, [ - arrow.Arrow(2013, 2, 3, 4, 5, 6), - arrow.Arrow(2013, 3, 3, 4, 5, 6), - arrow.Arrow(2013, 4, 3, 4, 5, 6), - arrow.Arrow(2013, 5, 3, 4, 5, 6), - ]) + result = list( + arrow.Arrow.range( + "month", datetime(2013, 2, 3, 4, 5, 6), datetime(2013, 5, 6, 7, 8, 9) + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 2, 3, 4, 5, 6), + arrow.Arrow(2013, 3, 3, 4, 5, 6), + arrow.Arrow(2013, 4, 3, 4, 5, 6), + arrow.Arrow(2013, 5, 3, 4, 5, 6), + ], + ) def test_week(self): - result = list(arrow.Arrow.range('week', datetime(2013, 9, 1, 2, 3, 4), - datetime(2013, 10, 1, 2, 3, 4))) - - assertEqual(result, [ - arrow.Arrow(2013, 9, 1, 2, 3, 4), - arrow.Arrow(2013, 9, 8, 2, 3, 4), - arrow.Arrow(2013, 9, 15, 2, 3, 4), - arrow.Arrow(2013, 9, 22, 2, 3, 4), - arrow.Arrow(2013, 9, 29, 2, 3, 4) - ]) + result = list( + arrow.Arrow.range( + "week", datetime(2013, 9, 1, 2, 3, 4), datetime(2013, 10, 1, 2, 3, 4) + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 9, 1, 2, 3, 4), + arrow.Arrow(2013, 9, 8, 2, 3, 4), + arrow.Arrow(2013, 9, 15, 2, 3, 4), + arrow.Arrow(2013, 9, 22, 2, 3, 4), + arrow.Arrow(2013, 9, 29, 2, 3, 4), + ], + ) def test_day(self): - result = list(arrow.Arrow.range('day', datetime(2013, 1, 2, 3, 4, 5), - datetime(2013, 1, 5, 6, 7, 8))) - - assertEqual(result, [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 3, 3, 4, 5), - arrow.Arrow(2013, 1, 4, 3, 4, 5), - arrow.Arrow(2013, 1, 5, 3, 4, 5), - ]) + result = list( + arrow.Arrow.range( + "day", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 5, 6, 7, 8) + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 3, 3, 4, 5), + arrow.Arrow(2013, 1, 4, 3, 4, 5), + arrow.Arrow(2013, 1, 5, 3, 4, 5), + ], + ) def test_hour(self): - result = list(arrow.Arrow.range('hour', datetime(2013, 1, 2, 3, 4, 5), - datetime(2013, 1, 2, 6, 7, 8))) - - assertEqual(result, [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 2, 4, 4, 5), - arrow.Arrow(2013, 1, 2, 5, 4, 5), - arrow.Arrow(2013, 1, 2, 6, 4, 5), - ]) - - result = list(arrow.Arrow.range('hour', datetime(2013, 1, 2, 3, 4, 5), - datetime(2013, 1, 2, 3, 4, 5))) - - assertEqual(result, [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - ]) + result = list( + arrow.Arrow.range( + "hour", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 6, 7, 8) + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 2, 4, 4, 5), + arrow.Arrow(2013, 1, 2, 5, 4, 5), + arrow.Arrow(2013, 1, 2, 6, 4, 5), + ], + ) + + result = list( + arrow.Arrow.range( + "hour", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 3, 4, 5) + ) + ) + + self.assertEqual(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))) - - assertEqual(result, [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 2, 3, 5, 5), - arrow.Arrow(2013, 1, 2, 3, 6, 5), - arrow.Arrow(2013, 1, 2, 3, 7, 5), - ]) + result = list( + arrow.Arrow.range( + "minute", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 3, 7, 8) + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 2, 3, 5, 5), + arrow.Arrow(2013, 1, 2, 3, 6, 5), + arrow.Arrow(2013, 1, 2, 3, 7, 5), + ], + ) def test_second(self): - result = list(arrow.Arrow.range('second', datetime(2013, 1, 2, 3, 4, 5), - datetime(2013, 1, 2, 3, 4, 8))) - - assertEqual(result, [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 2, 3, 4, 6), - arrow.Arrow(2013, 1, 2, 3, 4, 7), - arrow.Arrow(2013, 1, 2, 3, 4, 8), - ]) + result = list( + arrow.Arrow.range( + "second", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 3, 4, 8) + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 2, 3, 4, 6), + arrow.Arrow(2013, 1, 2, 3, 4, 7), + arrow.Arrow(2013, 1, 2, 3, 4, 8), + ], + ) def test_arrow(self): - result = list(arrow.Arrow.range('day', arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 5, 6, 7, 8))) - - assertEqual(result, [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 3, 3, 4, 5), - arrow.Arrow(2013, 1, 4, 3, 4, 5), - arrow.Arrow(2013, 1, 5, 3, 4, 5), - ]) + result = list( + arrow.Arrow.range( + "day", + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 5, 6, 7, 8), + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 3, 3, 4, 5), + arrow.Arrow(2013, 1, 4, 3, 4, 5), + arrow.Arrow(2013, 1, 5, 3, 4, 5), + ], + ) def test_naive_tz(self): - result = arrow.Arrow.range('year', datetime(2013, 1, 2, 3), datetime(2016, 4, 5, 6), - 'US/Pacific') + result = arrow.Arrow.range( + "year", datetime(2013, 1, 2, 3), datetime(2016, 4, 5, 6), "US/Pacific" + ) - [assertEqual(r.tzinfo, tz.gettz('US/Pacific')) for r in result] + [self.assertEqual(r.tzinfo, tz.gettz("US/Pacific")) for r in result] 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'))) + 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")), + ) - [assertEqual(r.tzinfo, tz.gettz('US/Pacific')) for r in result] + [self.assertEqual(r.tzinfo, tz.gettz("US/Pacific")) for r in result] 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'))) + result = arrow.Arrow.range( + "day", + datetime(2013, 1, 1, tzinfo=tz.gettz("US/Eastern")), + datetime(2013, 1, 3, tzinfo=tz.gettz("US/Pacific")), + ) - [assertEqual(r.tzinfo, tz.gettz('US/Eastern')) for r in result] + [self.assertEqual(r.tzinfo, tz.gettz("US/Eastern")) for r in result] 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')) + 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"), + ) - [assertEqual(r.tzinfo, tz.gettz('US/Central')) for r in result] + [self.assertEqual(r.tzinfo, tz.gettz("US/Central")) for r in result] def test_unsupported(self): - with assertRaises(AttributeError): - next(arrow.Arrow.range('abc', datetime.utcnow(), datetime.utcnow())) + with self.assertRaises(AttributeError): + next(arrow.Arrow.range("abc", datetime.utcnow(), datetime.utcnow())) class ArrowSpanRangeTests(Chai): - def test_year(self): - result = list(arrow.Arrow.span_range('year', datetime(2013, 2, 1), datetime(2016, 3, 31))) - - assertEqual(result, [ - (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 12, 31, 23, 59, 59, 999999)), - (arrow.Arrow(2014, 1, 1), arrow.Arrow(2014, 12, 31, 23, 59, 59, 999999)), - (arrow.Arrow(2015, 1, 1), arrow.Arrow(2015, 12, 31, 23, 59, 59, 999999)), - (arrow.Arrow(2016, 1, 1), arrow.Arrow(2016, 12, 31, 23, 59, 59, 999999)), - ]) + result = list( + arrow.Arrow.span_range("year", datetime(2013, 2, 1), datetime(2016, 3, 31)) + ) + + self.assertEqual( + result, + [ + ( + arrow.Arrow(2013, 1, 1), + arrow.Arrow(2013, 12, 31, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2014, 1, 1), + arrow.Arrow(2014, 12, 31, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2015, 1, 1), + arrow.Arrow(2015, 12, 31, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2016, 1, 1), + arrow.Arrow(2016, 12, 31, 23, 59, 59, 999999), + ), + ], + ) def test_quarter(self): - result = list(arrow.Arrow.span_range('quarter', datetime(2013, 2, 2), datetime(2013, 5, 15))) + result = list( + arrow.Arrow.span_range( + "quarter", datetime(2013, 2, 2), datetime(2013, 5, 15) + ) + ) - assertEqual(result, [ - (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 6, 30, 23, 59, 59, 999999)), - ]) + self.assertEqual( + result, + [ + (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 6, 30, 23, 59, 59, 999999)), + ], + ) def test_month(self): - result = list(arrow.Arrow.span_range('month', datetime(2013, 1, 2), datetime(2013, 4, 15))) + result = list( + arrow.Arrow.span_range("month", datetime(2013, 1, 2), datetime(2013, 4, 15)) + ) - assertEqual(result, [ - (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 1, 31, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 2, 1), arrow.Arrow(2013, 2, 28, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 3, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 4, 30, 23, 59, 59, 999999)), - ]) + self.assertEqual( + result, + [ + (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 1, 31, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 2, 1), arrow.Arrow(2013, 2, 28, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 3, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 4, 30, 23, 59, 59, 999999)), + ], + ) def test_week(self): - result = list(arrow.Arrow.span_range('week', datetime(2013, 2, 2), datetime(2013, 2, 28))) - - assertEqual(result, [ - (arrow.Arrow(2013, 1, 28), arrow.Arrow(2013, 2, 3, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 2, 4), arrow.Arrow(2013, 2, 10, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 2, 11), arrow.Arrow(2013, 2, 17, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 2, 18), arrow.Arrow(2013, 2, 24, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 2, 25), arrow.Arrow(2013, 3, 3, 23, 59, 59, 999999)), - ]) - + result = list( + arrow.Arrow.span_range("week", datetime(2013, 2, 2), datetime(2013, 2, 28)) + ) + + self.assertEqual( + result, + [ + (arrow.Arrow(2013, 1, 28), arrow.Arrow(2013, 2, 3, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 2, 4), arrow.Arrow(2013, 2, 10, 23, 59, 59, 999999)), + ( + arrow.Arrow(2013, 2, 11), + arrow.Arrow(2013, 2, 17, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 2, 18), + arrow.Arrow(2013, 2, 24, 23, 59, 59, 999999), + ), + (arrow.Arrow(2013, 2, 25), arrow.Arrow(2013, 3, 3, 23, 59, 59, 999999)), + ], + ) def test_day(self): - result = list(arrow.Arrow.span_range('day', datetime(2013, 1, 1, 12), - datetime(2013, 1, 4, 12))) - - assertEqual(result, [ - (arrow.Arrow(2013, 1, 1, 0), arrow.Arrow(2013, 1, 1, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 1, 2, 0), arrow.Arrow(2013, 1, 2, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 1, 3, 0), arrow.Arrow(2013, 1, 3, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 1, 4, 0), arrow.Arrow(2013, 1, 4, 23, 59, 59, 999999)), - ]) + result = list( + arrow.Arrow.span_range( + "day", datetime(2013, 1, 1, 12), datetime(2013, 1, 4, 12) + ) + ) + + self.assertEqual( + result, + [ + ( + arrow.Arrow(2013, 1, 1, 0), + arrow.Arrow(2013, 1, 1, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 2, 0), + arrow.Arrow(2013, 1, 2, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 3, 0), + arrow.Arrow(2013, 1, 3, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 4, 0), + arrow.Arrow(2013, 1, 4, 23, 59, 59, 999999), + ), + ], + ) def test_hour(self): - result = list(arrow.Arrow.span_range('hour', datetime(2013, 1, 1, 0, 30), - datetime(2013, 1, 1, 3, 30))) - - assertEqual(result, [ - (arrow.Arrow(2013, 1, 1, 0), arrow.Arrow(2013, 1, 1, 0, 59, 59, 999999)), - (arrow.Arrow(2013, 1, 1, 1), arrow.Arrow(2013, 1, 1, 1, 59, 59, 999999)), - (arrow.Arrow(2013, 1, 1, 2), arrow.Arrow(2013, 1, 1, 2, 59, 59, 999999)), - (arrow.Arrow(2013, 1, 1, 3), arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999)), - ]) - - result = list(arrow.Arrow.span_range('hour', datetime(2013, 1, 1, 3, 30), - datetime(2013, 1, 1, 3, 30))) - - assertEqual(result, [ - (arrow.Arrow(2013, 1, 1, 3), arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999)), - ]) + result = list( + arrow.Arrow.span_range( + "hour", datetime(2013, 1, 1, 0, 30), datetime(2013, 1, 1, 3, 30) + ) + ) + + self.assertEqual( + result, + [ + ( + arrow.Arrow(2013, 1, 1, 0), + arrow.Arrow(2013, 1, 1, 0, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 1), + arrow.Arrow(2013, 1, 1, 1, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 2), + arrow.Arrow(2013, 1, 1, 2, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 3), + arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999), + ), + ], + ) + + result = list( + arrow.Arrow.span_range( + "hour", datetime(2013, 1, 1, 3, 30), datetime(2013, 1, 1, 3, 30) + ) + ) + + self.assertEqual( + result, + [(arrow.Arrow(2013, 1, 1, 3), arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999))], + ) 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))) - - assertEqual(result, [ - (arrow.Arrow(2013, 1, 1, 0, 0), arrow.Arrow(2013, 1, 1, 0, 0, 59, 999999)), - (arrow.Arrow(2013, 1, 1, 0, 1), arrow.Arrow(2013, 1, 1, 0, 1, 59, 999999)), - (arrow.Arrow(2013, 1, 1, 0, 2), arrow.Arrow(2013, 1, 1, 0, 2, 59, 999999)), - (arrow.Arrow(2013, 1, 1, 0, 3), arrow.Arrow(2013, 1, 1, 0, 3, 59, 999999)), - ]) + result = list( + arrow.Arrow.span_range( + "minute", datetime(2013, 1, 1, 0, 0, 30), datetime(2013, 1, 1, 0, 3, 30) + ) + ) + + self.assertEqual( + result, + [ + ( + arrow.Arrow(2013, 1, 1, 0, 0), + arrow.Arrow(2013, 1, 1, 0, 0, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 0, 1), + arrow.Arrow(2013, 1, 1, 0, 1, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 0, 2), + arrow.Arrow(2013, 1, 1, 0, 2, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 0, 3), + arrow.Arrow(2013, 1, 1, 0, 3, 59, 999999), + ), + ], + ) def test_second(self): - result = list(arrow.Arrow.span_range('second', datetime(2013, 1, 1), - datetime(2013, 1, 1, 0, 0, 3))) - - assertEqual(result, [ - (arrow.Arrow(2013, 1, 1, 0, 0, 0), arrow.Arrow(2013, 1, 1, 0, 0, 0, 999999)), - (arrow.Arrow(2013, 1, 1, 0, 0, 1), arrow.Arrow(2013, 1, 1, 0, 0, 1, 999999)), - (arrow.Arrow(2013, 1, 1, 0, 0, 2), arrow.Arrow(2013, 1, 1, 0, 0, 2, 999999)), - (arrow.Arrow(2013, 1, 1, 0, 0, 3), arrow.Arrow(2013, 1, 1, 0, 0, 3, 999999)), - ]) + result = list( + arrow.Arrow.span_range( + "second", datetime(2013, 1, 1), datetime(2013, 1, 1, 0, 0, 3) + ) + ) + + self.assertEqual( + result, + [ + ( + arrow.Arrow(2013, 1, 1, 0, 0, 0), + arrow.Arrow(2013, 1, 1, 0, 0, 0, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 0, 0, 1), + arrow.Arrow(2013, 1, 1, 0, 0, 1, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 0, 0, 2), + arrow.Arrow(2013, 1, 1, 0, 0, 2, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 0, 0, 3), + arrow.Arrow(2013, 1, 1, 0, 0, 3, 999999), + ), + ], + ) def test_naive_tz(self): - tzinfo = tz.gettz('US/Pacific') + tzinfo = tz.gettz("US/Pacific") - result = arrow.Arrow.span_range('hour', datetime(2013, 1, 1, 0), - datetime(2013, 1, 1, 3, 59), 'US/Pacific') + result = arrow.Arrow.span_range( + "hour", datetime(2013, 1, 1, 0), datetime(2013, 1, 1, 3, 59), "US/Pacific" + ) for f, c in result: - assertEqual(f.tzinfo, tzinfo) - assertEqual(c.tzinfo, tzinfo) + self.assertEqual(f.tzinfo, tzinfo) + self.assertEqual(c.tzinfo, tzinfo) def test_aware_same_tz(self): - tzinfo = tz.gettz('US/Pacific') + tzinfo = tz.gettz("US/Pacific") - result = arrow.Arrow.span_range('hour', datetime(2013, 1, 1, 0, tzinfo=tzinfo), - datetime(2013, 1, 1, 2, 59, tzinfo=tzinfo)) + result = arrow.Arrow.span_range( + "hour", + datetime(2013, 1, 1, 0, tzinfo=tzinfo), + datetime(2013, 1, 1, 2, 59, tzinfo=tzinfo), + ) for f, c in result: - assertEqual(f.tzinfo, tzinfo) - assertEqual(c.tzinfo, tzinfo) + self.assertEqual(f.tzinfo, tzinfo) + self.assertEqual(c.tzinfo, tzinfo) def test_aware_different_tz(self): - tzinfo1 = tz.gettz('US/Pacific') - tzinfo2 = tz.gettz('US/Eastern') + tzinfo1 = tz.gettz("US/Pacific") + tzinfo2 = tz.gettz("US/Eastern") - result = arrow.Arrow.span_range('hour', datetime(2013, 1, 1, 0, tzinfo=tzinfo1), - datetime(2013, 1, 1, 2, 59, tzinfo=tzinfo2)) + result = arrow.Arrow.span_range( + "hour", + datetime(2013, 1, 1, 0, tzinfo=tzinfo1), + datetime(2013, 1, 1, 2, 59, tzinfo=tzinfo2), + ) for f, c in result: - assertEqual(f.tzinfo, tzinfo1) - assertEqual(c.tzinfo, tzinfo1) + self.assertEqual(f.tzinfo, tzinfo1) + self.assertEqual(c.tzinfo, tzinfo1) 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')), - tz='US/Central') + 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")), + tz="US/Central", + ) for f, c in result: - assertEqual(f.tzinfo, tz.gettz('US/Central')) - assertEqual(c.tzinfo, tz.gettz('US/Central')) + self.assertEqual(f.tzinfo, tz.gettz("US/Central")) + self.assertEqual(c.tzinfo, tz.gettz("US/Central")) -class ArrowIntervalTests(Chai): +class ArrowIntervalTests(Chai): def test_incorrect_input(self): correct = True try: - result = list(arrow.Arrow.interval('month', datetime(2013, 1, 2), datetime(2013, 4, 15), 0)) - except: + list( + arrow.Arrow.interval( + "month", datetime(2013, 1, 2), datetime(2013, 4, 15), 0 + ) + ) + except: # noqa: E722 correct = False - assertEqual(correct,False) + self.assertEqual(correct, False) def test_correct(self): - result = list(arrow.Arrow.interval('hour', datetime(2013, 5, 5, 12, 30), datetime(2013, 5, 5, 17, 15), 2)) + result = list( + arrow.Arrow.interval( + "hour", datetime(2013, 5, 5, 12, 30), datetime(2013, 5, 5, 17, 15), 2 + ) + ) + + self.assertEqual( + result, + [ + ( + arrow.Arrow(2013, 5, 5, 12), + arrow.Arrow(2013, 5, 5, 13, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 14), + arrow.Arrow(2013, 5, 5, 15, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 16), + arrow.Arrow(2013, 5, 5, 17, 59, 59, 999999), + ), + ], + ) - assertEqual(result,[(arrow.Arrow(2013, 5, 5, 12), arrow.Arrow(2013, 5, 5, 13, 59, 59, 999999)), - (arrow.Arrow(2013, 5, 5, 14), arrow.Arrow(2013, 5, 5, 15, 59, 59, 999999)), - (arrow.Arrow(2013, 5, 5, 16), arrow.Arrow(2013, 5, 5, 17, 59, 59, 999999))]) class ArrowSpanTests(Chai): - def setUp(self): super(ArrowSpanTests, self).setUp() @@ -1015,100 +1275,119 @@ def setUp(self): def test_span_attribute(self): - with assertRaises(AttributeError): - self.arrow.span('span') + with self.assertRaises(AttributeError): + self.arrow.span("span") def test_span_year(self): - floor, ceil = self.arrow.span('year') - - assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) - assertEqual(ceil, datetime(2013, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc())) + floor, ceil = self.arrow.span("year") + self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + ) def test_span_quarter(self): - floor, ceil = self.arrow.span('quarter') - - assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) - assertEqual(ceil, datetime(2013, 3, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc())) + floor, ceil = self.arrow.span("quarter") + self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + self.assertEqual( + 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) - - assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) - assertEqual(ceil, datetime(2013, 6, 30, 23, 59, 59, 999999, tzinfo=tz.tzutc())) + floor, ceil = self.arrow.span("quarter", 2) + self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + self.assertEqual( + 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) - - assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) - assertEqual(ceil, datetime(2014, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc())) + floor, ceil = self.arrow.span("year", 2) + self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2014, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + ) def test_span_month(self): - floor, ceil = self.arrow.span('month') + floor, ceil = self.arrow.span("month") - assertEqual(floor, datetime(2013, 2, 1, tzinfo=tz.tzutc())) - assertEqual(ceil, datetime(2013, 2, 28, 23, 59, 59, 999999, tzinfo=tz.tzutc())) + self.assertEqual(floor, datetime(2013, 2, 1, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 2, 28, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + ) def test_span_week(self): - floor, ceil = self.arrow.span('week') + floor, ceil = self.arrow.span("week") - assertEqual(floor, datetime(2013, 2, 11, tzinfo=tz.tzutc())) - assertEqual(ceil, datetime(2013, 2, 17, 23, 59, 59, 999999, tzinfo=tz.tzutc())) + self.assertEqual(floor, datetime(2013, 2, 11, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 2, 17, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + ) def test_span_day(self): - floor, ceil = self.arrow.span('day') + floor, ceil = self.arrow.span("day") - assertEqual(floor, datetime(2013, 2, 15, tzinfo=tz.tzutc())) - assertEqual(ceil, datetime(2013, 2, 15, 23, 59, 59, 999999, tzinfo=tz.tzutc())) + self.assertEqual(floor, datetime(2013, 2, 15, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 2, 15, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + ) def test_span_hour(self): - floor, ceil = self.arrow.span('hour') + floor, ceil = self.arrow.span("hour") - assertEqual(floor, datetime(2013, 2, 15, 3, tzinfo=tz.tzutc())) - assertEqual(ceil, datetime(2013, 2, 15, 3, 59, 59, 999999, tzinfo=tz.tzutc())) + self.assertEqual(floor, datetime(2013, 2, 15, 3, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 2, 15, 3, 59, 59, 999999, tzinfo=tz.tzutc()) + ) def test_span_minute(self): - floor, ceil = self.arrow.span('minute') + floor, ceil = self.arrow.span("minute") - assertEqual(floor, datetime(2013, 2, 15, 3, 41, tzinfo=tz.tzutc())) - assertEqual(ceil, datetime(2013, 2, 15, 3, 41, 59, 999999, tzinfo=tz.tzutc())) + self.assertEqual(floor, datetime(2013, 2, 15, 3, 41, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 2, 15, 3, 41, 59, 999999, tzinfo=tz.tzutc()) + ) def test_span_second(self): - floor, ceil = self.arrow.span('second') + floor, ceil = self.arrow.span("second") - assertEqual(floor, datetime(2013, 2, 15, 3, 41, 22, tzinfo=tz.tzutc())) - assertEqual(ceil, datetime(2013, 2, 15, 3, 41, 22, 999999, tzinfo=tz.tzutc())) + self.assertEqual(floor, datetime(2013, 2, 15, 3, 41, 22, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 2, 15, 3, 41, 22, 999999, tzinfo=tz.tzutc()) + ) def test_span_microsecond(self): - floor, ceil = self.arrow.span('microsecond') + floor, ceil = self.arrow.span("microsecond") - assertEqual(floor, datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc())) - assertEqual(ceil, datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc())) + self.assertEqual( + floor, datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) + ) + self.assertEqual( + ceil, datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) + ) def test_floor(self): - floor, ceil = self.arrow.span('month') + floor, ceil = self.arrow.span("month") - assertEqual(floor, self.arrow.floor('month')) - assertEqual(ceil, self.arrow.ceil('month')) + self.assertEqual(floor, self.arrow.floor("month")) + self.assertEqual(ceil, self.arrow.ceil("month")) class ArrowHumanizeTests(Chai): - def setUp(self): super(ArrowHumanizeTests, self).setUp() @@ -1117,189 +1396,228 @@ def setUp(self): def test_granularity(self): - assertEqual(self.now.humanize(granularity = 'second'), 'just now') + self.assertEqual(self.now.humanize(granularity="second"), "just now") later1 = self.now.shift(seconds=1) - assertEqual(self.now.humanize(later1, granularity = 'second'), 'just now') - assertEqual(later1.humanize(self.now, granularity = 'second'), 'just now') - assertEqual(self.now.humanize(later1, granularity = 'minute'), '0 minutes ago') - assertEqual(later1.humanize(self.now, granularity = 'minute'), 'in 0 minutes') + self.assertEqual(self.now.humanize(later1, granularity="second"), "just now") + self.assertEqual(later1.humanize(self.now, granularity="second"), "just now") + self.assertEqual( + self.now.humanize(later1, granularity="minute"), "0 minutes ago" + ) + self.assertEqual( + later1.humanize(self.now, granularity="minute"), "in 0 minutes" + ) later100 = self.now.shift(seconds=100) - assertEqual(self.now.humanize(later100, granularity = 'second'), 'seconds ago') - assertEqual(later100.humanize(self.now, granularity = 'second'), 'in seconds') - assertEqual(self.now.humanize(later100, granularity = 'minute'), 'a minute ago') - assertEqual(later100.humanize(self.now, granularity = 'minute'), 'in a minute') - assertEqual(self.now.humanize(later100, granularity = 'hour'), '0 hours ago') - assertEqual(later100.humanize(self.now, granularity = 'hour'), 'in 0 hours') + self.assertEqual( + self.now.humanize(later100, granularity="second"), "seconds ago" + ) + self.assertEqual( + later100.humanize(self.now, granularity="second"), "in seconds" + ) + self.assertEqual( + self.now.humanize(later100, granularity="minute"), "a minute ago" + ) + self.assertEqual( + later100.humanize(self.now, granularity="minute"), "in a minute" + ) + self.assertEqual(self.now.humanize(later100, granularity="hour"), "0 hours ago") + self.assertEqual(later100.humanize(self.now, granularity="hour"), "in 0 hours") later4000 = self.now.shift(seconds=4000) - assertEqual(self.now.humanize(later4000, granularity = 'minute'), '66 minutes ago') - assertEqual(later4000.humanize(self.now, granularity = 'minute'), 'in 66 minutes') - assertEqual(self.now.humanize(later4000, granularity = 'hour'), 'an hour ago') - assertEqual(later4000.humanize(self.now, granularity = 'hour'), 'in an hour') - assertEqual(self.now.humanize(later4000, granularity = 'day'), '0 days ago') - assertEqual(later4000.humanize(self.now, granularity = 'day'), 'in 0 days') + self.assertEqual( + self.now.humanize(later4000, granularity="minute"), "66 minutes ago" + ) + self.assertEqual( + later4000.humanize(self.now, granularity="minute"), "in 66 minutes" + ) + self.assertEqual( + self.now.humanize(later4000, granularity="hour"), "an hour ago" + ) + self.assertEqual(later4000.humanize(self.now, granularity="hour"), "in an hour") + self.assertEqual(self.now.humanize(later4000, granularity="day"), "0 days ago") + self.assertEqual(later4000.humanize(self.now, granularity="day"), "in 0 days") later105 = self.now.shift(seconds=10 ** 5) - assertEqual(self.now.humanize(later105, granularity = 'hour'), '27 hours ago') - assertEqual(later105.humanize(self.now, granularity = 'hour'), 'in 27 hours') - assertEqual(self.now.humanize(later105, granularity = 'day'), 'a day ago') - assertEqual(later105.humanize(self.now, granularity = 'day'), 'in a day') - assertEqual(self.now.humanize(later105, granularity = 'month'), '0 months ago') - assertEqual(later105.humanize(self.now, granularity = 'month'), 'in 0 months') + self.assertEqual( + self.now.humanize(later105, granularity="hour"), "27 hours ago" + ) + self.assertEqual(later105.humanize(self.now, granularity="hour"), "in 27 hours") + self.assertEqual(self.now.humanize(later105, granularity="day"), "a day ago") + self.assertEqual(later105.humanize(self.now, granularity="day"), "in a day") + self.assertEqual( + self.now.humanize(later105, granularity="month"), "0 months ago" + ) + self.assertEqual( + later105.humanize(self.now, granularity="month"), "in 0 months" + ) later106 = self.now.shift(seconds=3 * 10 ** 6) - assertEqual(self.now.humanize(later106, granularity = 'day'), '34 days ago') - assertEqual(later106.humanize(self.now, granularity = 'day'), 'in 34 days') - assertEqual(self.now.humanize(later106, granularity = 'month'), 'a month ago') - assertEqual(later106.humanize(self.now, granularity = 'month'), 'in a month') - assertEqual(self.now.humanize(later106, granularity = 'year'), '0 years ago') - assertEqual(later106.humanize(self.now, granularity = 'year'), 'in 0 years') + self.assertEqual(self.now.humanize(later106, granularity="day"), "34 days ago") + self.assertEqual(later106.humanize(self.now, granularity="day"), "in 34 days") + self.assertEqual( + self.now.humanize(later106, granularity="month"), "a month ago" + ) + self.assertEqual(later106.humanize(self.now, granularity="month"), "in a month") + self.assertEqual(self.now.humanize(later106, granularity="year"), "0 years ago") + self.assertEqual(later106.humanize(self.now, granularity="year"), "in 0 years") later506 = self.now.shift(seconds=50 * 10 ** 6) - assertEqual(self.now.humanize(later506, granularity = 'month'), '18 months ago') - assertEqual(later506.humanize(self.now, granularity = 'month'), 'in 18 months') - assertEqual(self.now.humanize(later506, granularity = 'year'), 'a year ago') - assertEqual(later506.humanize(self.now, granularity = 'year'), 'in a year') + self.assertEqual( + self.now.humanize(later506, granularity="month"), "18 months ago" + ) + self.assertEqual( + later506.humanize(self.now, granularity="month"), "in 18 months" + ) + self.assertEqual(self.now.humanize(later506, granularity="year"), "a year ago") + self.assertEqual(later506.humanize(self.now, granularity="year"), "in a year") later108 = self.now.shift(seconds=10 ** 8) - assertEqual(self.now.humanize(later108, granularity = 'year'), '3 years ago') - assertEqual(later108.humanize(self.now, granularity = 'year'), 'in 3 years') + self.assertEqual(self.now.humanize(later108, granularity="year"), "3 years ago") + self.assertEqual(later108.humanize(self.now, granularity="year"), "in 3 years") later108onlydistance = self.now.shift(seconds=10 ** 8) - assertEqual(self.now.humanize(later108onlydistance , only_distance=True, granularity = 'year'), '3 years') - assertEqual(later108onlydistance .humanize(self.now, only_distance=True, granularity = 'year'), '3 years') - with assertRaises(AttributeError): - self.now.humanize(later108, granularity = 'years') + self.assertEqual( + self.now.humanize( + later108onlydistance, only_distance=True, granularity="year" + ), + "3 years", + ) + self.assertEqual( + later108onlydistance.humanize( + self.now, only_distance=True, granularity="year" + ), + "3 years", + ) + with self.assertRaises(AttributeError): + self.now.humanize(later108, granularity="years") def test_seconds(self): later = self.now.shift(seconds=10) - assertEqual(self.now.humanize(later), 'seconds ago') - assertEqual(later.humanize(self.now), 'in seconds') + self.assertEqual(self.now.humanize(later), "seconds ago") + self.assertEqual(later.humanize(self.now), "in seconds") - assertEqual(self.now.humanize(later, only_distance=True), 'seconds') - assertEqual(later.humanize(self.now, only_distance=True), 'seconds') + self.assertEqual(self.now.humanize(later, only_distance=True), "seconds") + self.assertEqual(later.humanize(self.now, only_distance=True), "seconds") def test_minute(self): later = self.now.shift(minutes=1) - assertEqual(self.now.humanize(later), 'a minute ago') - assertEqual(later.humanize(self.now), 'in a minute') - - assertEqual(self.now.humanize(later, only_distance=True), 'a minute') - assertEqual(later.humanize(self.now, only_distance=True), 'a minute') + self.assertEqual(self.now.humanize(later), "a minute ago") + self.assertEqual(later.humanize(self.now), "in a minute") + self.assertEqual(self.now.humanize(later, only_distance=True), "a minute") + self.assertEqual(later.humanize(self.now, only_distance=True), "a minute") def test_minutes(self): later = self.now.shift(minutes=2) - assertEqual(self.now.humanize(later), '2 minutes ago') - assertEqual(later.humanize(self.now), 'in 2 minutes') + self.assertEqual(self.now.humanize(later), "2 minutes ago") + self.assertEqual(later.humanize(self.now), "in 2 minutes") - assertEqual(self.now.humanize(later, only_distance=True), '2 minutes') - assertEqual(later.humanize(self.now, only_distance=True), '2 minutes') + self.assertEqual(self.now.humanize(later, only_distance=True), "2 minutes") + self.assertEqual(later.humanize(self.now, only_distance=True), "2 minutes") def test_hour(self): later = self.now.shift(hours=1) - assertEqual(self.now.humanize(later), 'an hour ago') - assertEqual(later.humanize(self.now), 'in an hour') + self.assertEqual(self.now.humanize(later), "an hour ago") + self.assertEqual(later.humanize(self.now), "in an hour") - assertEqual(self.now.humanize(later, only_distance=True), 'an hour') - assertEqual(later.humanize(self.now, only_distance=True), 'an hour') + self.assertEqual(self.now.humanize(later, only_distance=True), "an hour") + self.assertEqual(later.humanize(self.now, only_distance=True), "an hour") def test_hours(self): later = self.now.shift(hours=2) - assertEqual(self.now.humanize(later), '2 hours ago') - assertEqual(later.humanize(self.now), 'in 2 hours') + self.assertEqual(self.now.humanize(later), "2 hours ago") + self.assertEqual(later.humanize(self.now), "in 2 hours") - assertEqual(self.now.humanize(later, only_distance=True), '2 hours') - assertEqual(later.humanize(self.now, only_distance=True), '2 hours') + self.assertEqual(self.now.humanize(later, only_distance=True), "2 hours") + self.assertEqual(later.humanize(self.now, only_distance=True), "2 hours") def test_day(self): later = self.now.shift(days=1) - assertEqual(self.now.humanize(later), 'a day ago') - assertEqual(later.humanize(self.now), 'in a day') + self.assertEqual(self.now.humanize(later), "a day ago") + self.assertEqual(later.humanize(self.now), "in a day") - assertEqual(self.now.humanize(later, only_distance=True), 'a day') - assertEqual(later.humanize(self.now, only_distance=True), 'a day') + self.assertEqual(self.now.humanize(later, only_distance=True), "a day") + self.assertEqual(later.humanize(self.now, only_distance=True), "a day") def test_days(self): later = self.now.shift(days=2) - assertEqual(self.now.humanize(later), '2 days ago') - assertEqual(later.humanize(self.now), 'in 2 days') + self.assertEqual(self.now.humanize(later), "2 days ago") + self.assertEqual(later.humanize(self.now), "in 2 days") - assertEqual(self.now.humanize(later, only_distance=True), '2 days') - assertEqual(later.humanize(self.now, only_distance=True), '2 days') + self.assertEqual(self.now.humanize(later, only_distance=True), "2 days") + self.assertEqual(later.humanize(self.now, only_distance=True), "2 days") # Regression tests for humanize bug referenced in issue 541 later = self.now.shift(days=3) - assertEqual(later.humanize(), 'in 3 days') + self.assertEqual(later.humanize(), "in 3 days") later = self.now.shift(days=3, seconds=1) - assertEqual(later.humanize(), 'in 3 days') + self.assertEqual(later.humanize(), "in 3 days") later = self.now.shift(days=4) - assertEqual(later.humanize(), 'in 4 days') + self.assertEqual(later.humanize(), "in 4 days") def test_month(self): later = self.now.shift(months=1) - assertEqual(self.now.humanize(later), 'a month ago') - assertEqual(later.humanize(self.now), 'in a month') + self.assertEqual(self.now.humanize(later), "a month ago") + self.assertEqual(later.humanize(self.now), "in a month") - assertEqual(self.now.humanize(later, only_distance=True), 'a month') - assertEqual(later.humanize(self.now, only_distance=True), 'a month') + self.assertEqual(self.now.humanize(later, only_distance=True), "a month") + self.assertEqual(later.humanize(self.now, only_distance=True), "a month") def test_months(self): later = self.now.shift(months=2) earlier = self.now.shift(months=-2) - assertEqual(earlier.humanize(self.now), '2 months ago') - assertEqual(later.humanize(self.now), 'in 2 months') + self.assertEqual(earlier.humanize(self.now), "2 months ago") + self.assertEqual(later.humanize(self.now), "in 2 months") - assertEqual(self.now.humanize(later, only_distance=True), '2 months') - assertEqual(later.humanize(self.now, only_distance=True), '2 months') + self.assertEqual(self.now.humanize(later, only_distance=True), "2 months") + self.assertEqual(later.humanize(self.now, only_distance=True), "2 months") def test_year(self): later = self.now.shift(years=1) - assertEqual(self.now.humanize(later), 'a year ago') - assertEqual(later.humanize(self.now), 'in a year') + self.assertEqual(self.now.humanize(later), "a year ago") + self.assertEqual(later.humanize(self.now), "in a year") - assertEqual(self.now.humanize(later, only_distance=True), 'a year') - assertEqual(later.humanize(self.now, only_distance=True), 'a year') + self.assertEqual(self.now.humanize(later, only_distance=True), "a year") + self.assertEqual(later.humanize(self.now, only_distance=True), "a year") def test_years(self): later = self.now.shift(years=2) - assertEqual(self.now.humanize(later), '2 years ago') - assertEqual(later.humanize(self.now), 'in 2 years') + self.assertEqual(self.now.humanize(later), "2 years ago") + self.assertEqual(later.humanize(self.now), "in 2 years") - assertEqual(self.now.humanize(later, only_distance=True), '2 years') - assertEqual(later.humanize(self.now, only_distance=True), '2 years') + self.assertEqual(self.now.humanize(later, only_distance=True), "2 years") + self.assertEqual(later.humanize(self.now, only_distance=True), "2 years") arw = arrow.Arrow(2014, 7, 2) result = arw.humanize(self.datetime) - assertEqual(result, 'in 2 years') + self.assertEqual(result, "in 2 years") def test_arrow(self): @@ -1307,7 +1625,7 @@ def test_arrow(self): result = arw.humanize(arrow.Arrow.fromdatetime(self.datetime)) - assertEqual(result, 'just now') + self.assertEqual(result, "just now") def test_datetime_tzinfo(self): @@ -1315,21 +1633,21 @@ def test_datetime_tzinfo(self): result = arw.humanize(self.datetime.replace(tzinfo=tz.tzutc())) - assertEqual(result, 'just now') + self.assertEqual(result, "just now") def test_other(self): arw = arrow.Arrow.fromdatetime(self.datetime) - with assertRaises(TypeError): + with self.assertRaises(TypeError): arw.humanize(object()) def test_invalid_locale(self): arw = arrow.Arrow.fromdatetime(self.datetime) - with assertRaises(ValueError): - arw.humanize(locale='klingon') + with self.assertRaises(ValueError): + arw.humanize(locale="klingon") def test_none(self): @@ -1337,11 +1655,10 @@ def test_none(self): result = arw.humanize() - assertEqual(result, 'just now') + self.assertEqual(result, "just now") class ArrowHumanizeTestsWithLocale(Chai): - def setUp(self): super(ArrowHumanizeTestsWithLocale, self).setUp() @@ -1351,104 +1668,104 @@ def test_now(self): arw = arrow.Arrow(2013, 1, 1, 0, 0, 0) - result = arw.humanize(self.datetime, locale='ru') + result = arw.humanize(self.datetime, locale="ru") - assertEqual(result, 'сейчас') + self.assertEqual(result, "сейчас") def test_seconds(self): arw = arrow.Arrow(2013, 1, 1, 0, 0, 44) - result = arw.humanize(self.datetime, locale='ru') + result = arw.humanize(self.datetime, locale="ru") - assertEqual(result, 'через несколько секунд') + self.assertEqual(result, "через несколько секунд") def test_years(self): arw = arrow.Arrow(2011, 7, 2) - result = arw.humanize(self.datetime, locale='ru') + result = arw.humanize(self.datetime, locale="ru") - assertEqual(result, '2 года назад') + self.assertEqual(result, "2 года назад") -class ArrowIsBetweenTests(Chai): +class ArrowIsBetweenTests(Chai): def test_start_before_end(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) result = target.is_between(start, end) - assertFalse(result) + self.assertFalse(result) def test_exclusive_exclusive_bounds(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 27)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 10)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 36)) - result = target.is_between(start, end, '()') - assertTrue(result) + result = target.is_between(start, end, "()") + self.assertTrue(result) result = target.is_between(start, end) - assertTrue(result) + self.assertTrue(result) def test_exclusive_exclusive_bounds_same_date(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - result = target.is_between(start, end, '()') - assertFalse(result) + result = target.is_between(start, end, "()") + self.assertFalse(result) def test_inclusive_exclusive_bounds(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 6)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 4)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 6)) - result = target.is_between(start, end, '[)') - assertFalse(result) + result = target.is_between(start, end, "[)") + self.assertFalse(result) def test_exclusive_inclusive_bounds(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - result = target.is_between(start, end, '(]') - assertTrue(result) + result = target.is_between(start, end, "(]") + self.assertTrue(result) def test_inclusive_inclusive_bounds_same_date(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - result = target.is_between(start, end, '[]') - assertTrue(result) + result = target.is_between(start, end, "[]") + self.assertTrue(result) def test_type_error_exception(self): - with assertRaises(TypeError): + with self.assertRaises(TypeError): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = datetime(2013, 5, 5) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) - result = target.is_between(start, end) + target.is_between(start, end) - with assertRaises(TypeError): + with self.assertRaises(TypeError): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) end = datetime(2013, 5, 8) - result = target.is_between(start, end) + target.is_between(start, end) - with assertRaises(TypeError): - result = target.is_between(None, None) + with self.assertRaises(TypeError): + target.is_between(None, None) def test_attribute_error_exception(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) - with assertRaises(AttributeError): - result = target.is_between(start, end, "][") - with assertRaises(AttributeError): - result = target.is_between(start, end, "") - with assertRaises(AttributeError): - result = target.is_between(start, end, "]") - with assertRaises(AttributeError): - result = target.is_between(start, end, "[") - with assertRaises(AttributeError): - result = target.is_between(start, end, "hello") + with self.assertRaises(AttributeError): + target.is_between(start, end, "][") + with self.assertRaises(AttributeError): + target.is_between(start, end, "") + with self.assertRaises(AttributeError): + target.is_between(start, end, "]") + with self.assertRaises(AttributeError): + target.is_between(start, end, "[") + with self.assertRaises(AttributeError): + target.is_between(start, end, "hello") -class ArrowUtilTests(Chai): +class ArrowUtilTests(Chai): def test_get_datetime(self): get_datetime = arrow.Arrow._get_datetime @@ -1457,44 +1774,50 @@ def test_get_datetime(self): dt = datetime.utcnow() timestamp = time.time() - assertEqual(get_datetime(arw), arw.datetime) - assertEqual(get_datetime(dt), dt) - assertEqual(get_datetime(timestamp), arrow.Arrow.utcfromtimestamp(timestamp).datetime) + self.assertEqual(get_datetime(arw), arw.datetime) + self.assertEqual(get_datetime(dt), dt) + self.assertEqual( + get_datetime(timestamp), arrow.Arrow.utcfromtimestamp(timestamp).datetime + ) - with assertRaises(ValueError) as raise_ctx: - get_datetime('abc') - assertFalse('{}' in str(raise_ctx.exception)) + with self.assertRaises(ValueError) as raise_ctx: + get_datetime("abc") + self.assertFalse("{}" in str(raise_ctx.exception)) def test_get_tzinfo(self): get_tzinfo = arrow.Arrow._get_tzinfo - with assertRaises(ValueError) as raise_ctx: - get_tzinfo('abc') - assertFalse('{}' in str(raise_ctx.exception)) + with self.assertRaises(ValueError) as raise_ctx: + get_tzinfo("abc") + self.assertFalse("{}" in str(raise_ctx.exception)) def test_get_timestamp_from_input(self): - assertEqual(arrow.Arrow._get_timestamp_from_input(123), 123) - assertEqual(arrow.Arrow._get_timestamp_from_input(123.4), 123.4) - assertEqual(arrow.Arrow._get_timestamp_from_input('123'), 123.0) - assertEqual(arrow.Arrow._get_timestamp_from_input('123.4'), 123.4) + self.assertEqual(arrow.Arrow._get_timestamp_from_input(123), 123) + self.assertEqual(arrow.Arrow._get_timestamp_from_input(123.4), 123.4) + self.assertEqual(arrow.Arrow._get_timestamp_from_input("123"), 123.0) + self.assertEqual(arrow.Arrow._get_timestamp_from_input("123.4"), 123.4) - with assertRaises(ValueError): - arrow.Arrow._get_timestamp_from_input('abc') + with self.assertRaises(ValueError): + arrow.Arrow._get_timestamp_from_input("abc") def test_get_iteration_params(self): - assertEqual(arrow.Arrow._get_iteration_params('end', None), ('end', sys.maxsize)) - assertEqual(arrow.Arrow._get_iteration_params(None, 100), (arrow.Arrow.max, 100)) - assertEqual(arrow.Arrow._get_iteration_params(100, 120), (100, 120)) + self.assertEqual( + arrow.Arrow._get_iteration_params("end", None), ("end", sys.maxsize) + ) + self.assertEqual( + arrow.Arrow._get_iteration_params(None, 100), (arrow.Arrow.max, 100) + ) + self.assertEqual(arrow.Arrow._get_iteration_params(100, 120), (100, 120)) - with assertRaises(Exception): + with self.assertRaises(Exception): arrow.Arrow._get_iteration_params(None, None) def test_list_to_iter_shim(self): def newshim(): - return util.list_to_iter_shim(range(5), warn_text='testing') + return util.list_to_iter_shim(range(5), warn_text="testing") # Iterating over a shim once should not throw a warning with warnings.catch_warnings(record=True) as w: @@ -1502,22 +1825,25 @@ def newshim(): iter(newshim()) list(newshim()) - for _ in newshim(): pass + for _ in newshim(): + pass len(newshim()) # ...because it's called by `list(x)` - assertEqual([], w) + self.assertEqual([], w) # Iterating over a shim twice (or more) should throw a warning with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") shim = newshim() - for _ in shim: pass - for _ in shim: pass + for _ in shim: + pass + for _ in shim: + pass - assertEqual(1, len(w)) - assertEqual(w[0].category, DeprecationWarning) - assertEqual("testing", w[0].message.args[0]) + self.assertEqual(1, len(w)) + self.assertEqual(w[0].category, DeprecationWarning) + self.assertEqual("testing", w[0].message.args[0]) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") @@ -1546,9 +1872,9 @@ def newshim(): newshim().sort() if sys.version_info.major >= 3: # pragma: no cover - assertEqual(19, len(w)) + self.assertEqual(19, len(w)) else: # pragma: no cover - assertEqual(15, len(w)) + self.assertEqual(15, len(w)) for warn in w: - assertEqual(warn.category, DeprecationWarning) - assertEqual("testing", warn.message.args[0]) + self.assertEqual(warn.category, DeprecationWarning) + self.assertEqual("testing", warn.message.args[0]) diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 378c0f51c..21e5ae4f6 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -1,18 +1,19 @@ +# -*- coding: utf-8 -*- +import time +from datetime import date, datetime + from chai import Chai -from datetime import datetime, date from dateutil import tz -import time from arrow import factory, util def assertDtEqual(dt1, dt2, within=10): - assertEqual(dt1.tzinfo, dt2.tzinfo) - assertTrue(abs(util.total_seconds(dt1 - dt2)) < within) + assertEqual(dt1.tzinfo, dt2.tzinfo) # noqa: F821 + assertTrue(abs(util.total_seconds(dt1 - dt2)) < within) # noqa: F821 class GetTests(Chai): - def setUp(self): super(GetTests, self).setUp() @@ -24,38 +25,42 @@ def test_no_args(self): def test_timestamp_one_arg_no_arg(self): - no_arg = self.factory.get('1406430900').timestamp - one_arg = self.factory.get('1406430900', 'X').timestamp + no_arg = self.factory.get("1406430900").timestamp + one_arg = self.factory.get("1406430900", "X").timestamp - assertEqual(no_arg, one_arg) + self.assertEqual(no_arg, one_arg) def test_one_arg_non(self): - assertDtEqual(self.factory.get(None), datetime.utcnow().replace(tzinfo=tz.tzutc())) + assertDtEqual( + self.factory.get(None), datetime.utcnow().replace(tzinfo=tz.tzutc()) + ) def test_struct_time(self): - assertDtEqual(self.factory.get(time.gmtime()), - datetime.utcnow().replace(tzinfo=tz.tzutc())) + assertDtEqual( + self.factory.get(time.gmtime()), + datetime.utcnow().replace(tzinfo=tz.tzutc()), + ) def test_one_arg_timestamp(self): timestamp = 12345 timestamp_dt = datetime.utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) - assertEqual(self.factory.get(timestamp), timestamp_dt) - assertEqual(self.factory.get(str(timestamp)), timestamp_dt) + self.assertEqual(self.factory.get(timestamp), timestamp_dt) + self.assertEqual(self.factory.get(str(timestamp)), timestamp_dt) timestamp = 123.45 timestamp_dt = datetime.utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) - assertEqual(self.factory.get(timestamp), timestamp_dt) - assertEqual(self.factory.get(str(timestamp)), timestamp_dt) + self.assertEqual(self.factory.get(timestamp), timestamp_dt) + self.assertEqual(self.factory.get(str(timestamp)), timestamp_dt) # Issue 216 - timestamp = '99999999999999999999999999' + timestamp = "99999999999999999999999999" # Python 3 raises `OverflowError`, Python 2 raises `ValueError` - with assertRaises((OverflowError, ValueError,)): + with self.assertRaises((OverflowError, ValueError)): self.factory.get(timestamp) def test_one_arg_arrow(self): @@ -63,32 +68,40 @@ def test_one_arg_arrow(self): arw = self.factory.utcnow() result = self.factory.get(arw) - assertEqual(arw, result) + self.assertEqual(arw, result) def test_one_arg_datetime(self): dt = datetime.utcnow().replace(tzinfo=tz.tzutc()) - assertEqual(self.factory.get(dt), dt) + self.assertEqual(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()) - assertEqual(self.factory.get(d), dt) + self.assertEqual(self.factory.get(d), dt) def test_one_arg_tzinfo(self): - expected = datetime.utcnow().replace(tzinfo=tz.tzutc()).astimezone(tz.gettz('US/Pacific')) + self.expected = ( + datetime.utcnow() + .replace(tzinfo=tz.tzutc()) + .astimezone(tz.gettz("US/Pacific")) + ) - assertDtEqual(self.factory.get(tz.gettz('US/Pacific')), expected) + assertDtEqual(self.factory.get(tz.gettz("US/Pacific")), self.expected) def test_kwarg_tzinfo(self): - expected = datetime.utcnow().replace(tzinfo=tz.tzutc()).astimezone(tz.gettz('US/Pacific')) + self.expected = ( + datetime.utcnow() + .replace(tzinfo=tz.tzutc()) + .astimezone(tz.gettz("US/Pacific")) + ) - assertDtEqual(self.factory.get(tzinfo=tz.gettz('US/Pacific')), expected) + assertDtEqual(self.factory.get(tzinfo=tz.gettz("US/Pacific")), self.expected) def test_one_arg_iso_str(self): @@ -98,104 +111,118 @@ def test_one_arg_iso_str(self): def test_one_arg_other(self): - with assertRaises(TypeError): + with self.assertRaises(TypeError): self.factory.get(object()) def test_one_arg_bool(self): - with assertRaises(TypeError): + with self.assertRaises(TypeError): self.factory.get(False) - with assertRaises(TypeError): + with self.assertRaises(TypeError): self.factory.get(True) 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), tz.gettz("US/Pacific")) - assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz('US/Pacific'))) + self.assertEqual( + 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') + result = self.factory.get(datetime(2013, 1, 1), "US/Pacific") - assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz('US/Pacific'))) + self.assertEqual( + 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')) + result = self.factory.get(date(2013, 1, 1), tz.gettz("US/Pacific")) - assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz('US/Pacific'))) + self.assertEqual( + 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') + result = self.factory.get(date(2013, 1, 1), "US/Pacific") - assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz('US/Pacific'))) + self.assertEqual( + result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) + ) def test_two_args_datetime_other(self): - with assertRaises(TypeError): + with self.assertRaises(TypeError): self.factory.get(datetime.utcnow(), object()) def test_two_args_date_other(self): - with assertRaises(TypeError): + with self.assertRaises(TypeError): self.factory.get(date.today(), object()) def test_two_args_str_str(self): - result = self.factory.get('2013-01-01', 'YYYY-MM-DD') + result = self.factory.get("2013-01-01", "YYYY-MM-DD") - assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + self.assertEqual(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=tz.gettz("US/Pacific")) - assertDtEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz('US/Pacific'))) + assertDtEqual( + result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) + ) 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') + twitter_date = "Fri Apr 08 21:08:54 +0000 2016" + result = self.factory.get(twitter_date, "ddd MMM DD HH:mm:ss Z YYYY") - assertEqual(result._datetime, datetime(2016, 4, 8, 21, 8, 54, tzinfo=tz.tzutc())) + self.assertEqual( + 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']) - - assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + result = self.factory.get("2013-01-01", ["MM/DD/YYYY", "YYYY-MM-DD"]) + self.assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.tzutc())) def test_two_args_unicode_unicode(self): - result = self.factory.get(u'2013-01-01', u'YYYY-MM-DD') + result = self.factory.get(u"2013-01-01", u"YYYY-MM-DD") - assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + self.assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.tzutc())) def test_two_args_other(self): - with assertRaises(TypeError): + with self.assertRaises(TypeError): self.factory.get(object(), object()) def test_three_args_with_tzinfo(self): - timefmt = 'YYYYMMDD' - d = '20150514' + timefmt = "YYYYMMDD" + d = "20150514" - assertEqual(self.factory.get(d, timefmt, tzinfo=tz.tzlocal()), - datetime(2015, 5, 14, tzinfo=tz.tzlocal())) + self.assertEqual( + self.factory.get(d, timefmt, tzinfo=tz.tzlocal()), + datetime(2015, 5, 14, tzinfo=tz.tzlocal()), + ) def test_three_args(self): - assertEqual(self.factory.get(2013, 1, 1), datetime(2013, 1, 1, tzinfo=tz.tzutc())) + self.assertEqual( + self.factory.get(2013, 1, 1), datetime(2013, 1, 1, tzinfo=tz.tzutc()) + ) class UtcNowTests(Chai): - def setUp(self): super(UtcNowTests, self).setUp() @@ -203,11 +230,13 @@ def setUp(self): def test_utcnow(self): - assertDtEqual(self.factory.utcnow()._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc())) + assertDtEqual( + self.factory.utcnow()._datetime, + datetime.utcnow().replace(tzinfo=tz.tzutc()), + ) class NowTests(Chai): - def setUp(self): super(NowTests, self).setUp() @@ -219,8 +248,8 @@ def test_no_tz(self): def test_tzinfo(self): - assertDtEqual(self.factory.now(tz.gettz('EST')), datetime.now(tz.gettz('EST'))) + assertDtEqual(self.factory.now(tz.gettz("EST")), datetime.now(tz.gettz("EST"))) def test_tz_str(self): - assertDtEqual(self.factory.now('EST'), datetime.now(tz.gettz('EST'))) + assertDtEqual(self.factory.now("EST"), datetime.now(tz.gettz("EST"))) diff --git a/tests/formatter_tests.py b/tests/formatter_tests.py index a36039092..f28a0e851 100644 --- a/tests/formatter_tests.py +++ b/tests/formatter_tests.py @@ -1,14 +1,15 @@ +# -*- coding: utf-8 -*- +import time +from datetime import datetime + +import pytz from chai import Chai +from dateutil import tz as dateutil_tz from arrow import formatter -from datetime import datetime -from dateutil import tz as dateutil_tz -import pytz -import time class DateTimeFormatterFormatTokenTests(Chai): - def setUp(self): super(DateTimeFormatterFormatTokenTests, self).setUp() @@ -18,133 +19,134 @@ def test_format(self): dt = datetime(2013, 2, 5, 12, 32, 51) - result = self.formatter.format(dt, 'MM-DD-YYYY hh:mm:ss a') + result = self.formatter.format(dt, "MM-DD-YYYY hh:mm:ss a") - assertEqual(result, '02-05-2013 12:32:51 pm') + self.assertEqual(result, "02-05-2013 12:32:51 pm") def test_year(self): dt = datetime(2013, 1, 1) - assertEqual(self.formatter._format_token(dt, 'YYYY'), '2013') - assertEqual(self.formatter._format_token(dt, 'YY'), '13') + self.assertEqual(self.formatter._format_token(dt, "YYYY"), "2013") + self.assertEqual(self.formatter._format_token(dt, "YY"), "13") def test_month(self): dt = datetime(2013, 1, 1) - assertEqual(self.formatter._format_token(dt, 'MMMM'), 'January') - assertEqual(self.formatter._format_token(dt, 'MMM'), 'Jan') - assertEqual(self.formatter._format_token(dt, 'MM'), '01') - assertEqual(self.formatter._format_token(dt, 'M'), '1') + self.assertEqual(self.formatter._format_token(dt, "MMMM"), "January") + self.assertEqual(self.formatter._format_token(dt, "MMM"), "Jan") + self.assertEqual(self.formatter._format_token(dt, "MM"), "01") + self.assertEqual(self.formatter._format_token(dt, "M"), "1") def test_day(self): dt = datetime(2013, 2, 1) - assertEqual(self.formatter._format_token(dt, 'DDDD'), '032') - assertEqual(self.formatter._format_token(dt, 'DDD'), '32') - assertEqual(self.formatter._format_token(dt, 'DD'), '01') - assertEqual(self.formatter._format_token(dt, 'D'), '1') - assertEqual(self.formatter._format_token(dt, 'Do'), '1st') - + self.assertEqual(self.formatter._format_token(dt, "DDDD"), "032") + self.assertEqual(self.formatter._format_token(dt, "DDD"), "32") + self.assertEqual(self.formatter._format_token(dt, "DD"), "01") + self.assertEqual(self.formatter._format_token(dt, "D"), "1") + self.assertEqual(self.formatter._format_token(dt, "Do"), "1st") - assertEqual(self.formatter._format_token(dt, 'dddd'), 'Friday') - assertEqual(self.formatter._format_token(dt, 'ddd'), 'Fri') - assertEqual(self.formatter._format_token(dt, 'd'), '5') + self.assertEqual(self.formatter._format_token(dt, "dddd"), "Friday") + self.assertEqual(self.formatter._format_token(dt, "ddd"), "Fri") + self.assertEqual(self.formatter._format_token(dt, "d"), "5") def test_hour(self): dt = datetime(2013, 1, 1, 2) - assertEqual(self.formatter._format_token(dt, 'HH'), '02') - assertEqual(self.formatter._format_token(dt, 'H'), '2') + self.assertEqual(self.formatter._format_token(dt, "HH"), "02") + self.assertEqual(self.formatter._format_token(dt, "H"), "2") dt = datetime(2013, 1, 1, 13) - assertEqual(self.formatter._format_token(dt, 'HH'), '13') - assertEqual(self.formatter._format_token(dt, 'H'), '13') + self.assertEqual(self.formatter._format_token(dt, "HH"), "13") + self.assertEqual(self.formatter._format_token(dt, "H"), "13") dt = datetime(2013, 1, 1, 2) - assertEqual(self.formatter._format_token(dt, 'hh'), '02') - assertEqual(self.formatter._format_token(dt, 'h'), '2') + self.assertEqual(self.formatter._format_token(dt, "hh"), "02") + self.assertEqual(self.formatter._format_token(dt, "h"), "2") dt = datetime(2013, 1, 1, 13) - assertEqual(self.formatter._format_token(dt, 'hh'), '01') - assertEqual(self.formatter._format_token(dt, 'h'), '1') + self.assertEqual(self.formatter._format_token(dt, "hh"), "01") + self.assertEqual(self.formatter._format_token(dt, "h"), "1") # test that 12-hour time converts to '12' at midnight dt = datetime(2013, 1, 1, 0) - assertEqual(self.formatter._format_token(dt, 'hh'), '12') - assertEqual(self.formatter._format_token(dt, 'h'), '12') + self.assertEqual(self.formatter._format_token(dt, "hh"), "12") + self.assertEqual(self.formatter._format_token(dt, "h"), "12") def test_minute(self): dt = datetime(2013, 1, 1, 0, 1) - assertEqual(self.formatter._format_token(dt, 'mm'), '01') - assertEqual(self.formatter._format_token(dt, 'm'), '1') + self.assertEqual(self.formatter._format_token(dt, "mm"), "01") + self.assertEqual(self.formatter._format_token(dt, "m"), "1") def test_second(self): dt = datetime(2013, 1, 1, 0, 0, 1) - assertEqual(self.formatter._format_token(dt, 'ss'), '01') - assertEqual(self.formatter._format_token(dt, 's'), '1') + self.assertEqual(self.formatter._format_token(dt, "ss"), "01") + self.assertEqual(self.formatter._format_token(dt, "s"), "1") def test_sub_second(self): dt = datetime(2013, 1, 1, 0, 0, 0, 123456) - assertEqual(self.formatter._format_token(dt, 'SSSSSS'), '123456') - assertEqual(self.formatter._format_token(dt, 'SSSSS'), '12345') - assertEqual(self.formatter._format_token(dt, 'SSSS'), '1234') - assertEqual(self.formatter._format_token(dt, 'SSS'), '123') - assertEqual(self.formatter._format_token(dt, 'SS'), '12') - assertEqual(self.formatter._format_token(dt, 'S'), '1') + self.assertEqual(self.formatter._format_token(dt, "SSSSSS"), "123456") + self.assertEqual(self.formatter._format_token(dt, "SSSSS"), "12345") + self.assertEqual(self.formatter._format_token(dt, "SSSS"), "1234") + self.assertEqual(self.formatter._format_token(dt, "SSS"), "123") + self.assertEqual(self.formatter._format_token(dt, "SS"), "12") + self.assertEqual(self.formatter._format_token(dt, "S"), "1") dt = datetime(2013, 1, 1, 0, 0, 0, 2000) - assertEqual(self.formatter._format_token(dt, 'SSSSSS'), '002000') - assertEqual(self.formatter._format_token(dt, 'SSSSS'), '00200') - assertEqual(self.formatter._format_token(dt, 'SSSS'), '0020') - assertEqual(self.formatter._format_token(dt, 'SSS'), '002') - assertEqual(self.formatter._format_token(dt, 'SS'), '00') - assertEqual(self.formatter._format_token(dt, 'S'), '0') + self.assertEqual(self.formatter._format_token(dt, "SSSSSS"), "002000") + self.assertEqual(self.formatter._format_token(dt, "SSSSS"), "00200") + self.assertEqual(self.formatter._format_token(dt, "SSSS"), "0020") + self.assertEqual(self.formatter._format_token(dt, "SSS"), "002") + self.assertEqual(self.formatter._format_token(dt, "SS"), "00") + self.assertEqual(self.formatter._format_token(dt, "S"), "0") def test_timestamp(self): timestamp = time.time() dt = datetime.utcfromtimestamp(timestamp) - assertEqual(self.formatter._format_token(dt, 'X'), str(int(timestamp))) + self.assertEqual(self.formatter._format_token(dt, "X"), str(int(timestamp))) def test_timezone(self): - dt = datetime.utcnow().replace(tzinfo=dateutil_tz.gettz('US/Pacific')) + dt = datetime.utcnow().replace(tzinfo=dateutil_tz.gettz("US/Pacific")) - result = self.formatter._format_token(dt, 'ZZ') - assertTrue(result == '-07:00' or result == '-08:00') + result = self.formatter._format_token(dt, "ZZ") + self.assertTrue(result == "-07:00" or result == "-08:00") - result = self.formatter._format_token(dt, 'Z') - assertTrue(result == '-0700' or result == '-0800') + result = self.formatter._format_token(dt, "Z") + self.assertTrue(result == "-0700" or result == "-0800") def test_timezone_formatter(self): tz_map = { - #'BRST': 'America/Sao_Paulo', TODO investigate why this fails - 'CET': 'Europe/Berlin', - 'JST': 'Asia/Tokyo', - 'PST': 'US/Pacific', + # 'BRST': 'America/Sao_Paulo', TODO investigate why this fails + "CET": "Europe/Berlin", + "JST": "Asia/Tokyo", + "PST": "US/Pacific", } for abbreviation, full_name in tz_map.items(): # 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_name)) - result = self.formatter._format_token(dt, 'ZZZ') - assertEqual(result, abbreviation) + dt = datetime(1986, 2, 14, tzinfo=pytz.timezone("UTC")).replace( + tzinfo=dateutil_tz.gettz(full_name) + ) + result = self.formatter._format_token(dt, "ZZZ") + self.assertEqual(result, abbreviation) def test_am_pm(self): dt = datetime(2012, 1, 1, 11) - assertEqual(self.formatter._format_token(dt, 'a'), 'am') - assertEqual(self.formatter._format_token(dt, 'A'), 'AM') + self.assertEqual(self.formatter._format_token(dt, "a"), "am") + self.assertEqual(self.formatter._format_token(dt, "A"), "AM") dt = datetime(2012, 1, 1, 13) - assertEqual(self.formatter._format_token(dt, 'a'), 'pm') - assertEqual(self.formatter._format_token(dt, 'A'), 'PM') + self.assertEqual(self.formatter._format_token(dt, "a"), "pm") + self.assertEqual(self.formatter._format_token(dt, "A"), "PM") def test_nonsense(self): dt = datetime(2012, 1, 1, 11) - assertEqual(self.formatter._format_token(dt, None), None) - assertEqual(self.formatter._format_token(dt, 'NONSENSE'), None) + self.assertEqual(self.formatter._format_token(dt, None), None) + self.assertEqual(self.formatter._format_token(dt, "NONSENSE"), None) diff --git a/tests/locales_tests.py b/tests/locales_tests.py index adaab866f..c1a8912a3 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -1,36 +1,31 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from datetime import datetime - from chai import Chai -from arrow import locales -from arrow.api import now -from arrow import arrow +from arrow import arrow, locales -class ModuleTests(Chai): +class ModuleTests(Chai): def test_get_locale(self): - mock_locales = mock(locales, '_locales') - mock_locale_cls = mock() - mock_locale = mock() + mock_locales = self.mock(locales, "_locales") + mock_locale_cls = self.mock() + mock_locale = self.mock() - expect(mock_locales.get).args('name').returns(mock_locale_cls) - expect(mock_locale_cls).returns(mock_locale) + self.expect(mock_locales.get).args("name").returns(mock_locale_cls) + self.expect(mock_locale_cls).returns(mock_locale) - result = locales.get_locale('name') + result = locales.get_locale("name") - assertEqual(result, mock_locale) + self.assertEqual(result, mock_locale) def test_locales(self): - assertTrue(len(locales._locales) > 0) + self.assertTrue(len(locales._locales) > 0) class LocaleTests(Chai): - def setUp(self): super(LocaleTests, self).setUp() @@ -38,145 +33,138 @@ def setUp(self): def test_format_timeframe(self): - assertEqual(self.locale._format_timeframe('hours', 2), '2 hours') - assertEqual(self.locale._format_timeframe('hour', 0), 'an hour') + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 hours") + self.assertEqual(self.locale._format_timeframe("hour", 0), "an hour") def test_format_relative_now(self): - result = self.locale._format_relative('just now', 'now', 0) + result = self.locale._format_relative("just now", "now", 0) - assertEqual(result, 'just now') + self.assertEqual(result, "just now") def test_format_relative_past(self): - result = self.locale._format_relative('an hour', 'hour', 1) + result = self.locale._format_relative("an hour", "hour", 1) - assertEqual(result, 'in an hour') + self.assertEqual(result, "in an hour") def test_format_relative_future(self): - result = self.locale._format_relative('an hour', 'hour', -1) + result = self.locale._format_relative("an hour", "hour", -1) - assertEqual(result, 'an hour ago') + self.assertEqual(result, "an hour ago") def test_ordinal_number(self): - assertEqual(self.locale.ordinal_number(0), '0th') - assertEqual(self.locale.ordinal_number(1), '1st') - assertEqual(self.locale.ordinal_number(2), '2nd') - assertEqual(self.locale.ordinal_number(3), '3rd') - assertEqual(self.locale.ordinal_number(4), '4th') - assertEqual(self.locale.ordinal_number(10), '10th') - assertEqual(self.locale.ordinal_number(11), '11th') - assertEqual(self.locale.ordinal_number(12), '12th') - assertEqual(self.locale.ordinal_number(13), '13th') - assertEqual(self.locale.ordinal_number(14), '14th') - assertEqual(self.locale.ordinal_number(21), '21st') - assertEqual(self.locale.ordinal_number(22), '22nd') - assertEqual(self.locale.ordinal_number(23), '23rd') - assertEqual(self.locale.ordinal_number(24), '24th') - - assertEqual(self.locale.ordinal_number(100), '100th') - assertEqual(self.locale.ordinal_number(101), '101st') - assertEqual(self.locale.ordinal_number(102), '102nd') - assertEqual(self.locale.ordinal_number(103), '103rd') - assertEqual(self.locale.ordinal_number(104), '104th') - assertEqual(self.locale.ordinal_number(110), '110th') - assertEqual(self.locale.ordinal_number(111), '111th') - assertEqual(self.locale.ordinal_number(112), '112th') - assertEqual(self.locale.ordinal_number(113), '113th') - assertEqual(self.locale.ordinal_number(114), '114th') - assertEqual(self.locale.ordinal_number(121), '121st') - assertEqual(self.locale.ordinal_number(122), '122nd') - assertEqual(self.locale.ordinal_number(123), '123rd') - assertEqual(self.locale.ordinal_number(124), '124th') + self.assertEqual(self.locale.ordinal_number(0), "0th") + self.assertEqual(self.locale.ordinal_number(1), "1st") + self.assertEqual(self.locale.ordinal_number(2), "2nd") + self.assertEqual(self.locale.ordinal_number(3), "3rd") + self.assertEqual(self.locale.ordinal_number(4), "4th") + self.assertEqual(self.locale.ordinal_number(10), "10th") + self.assertEqual(self.locale.ordinal_number(11), "11th") + self.assertEqual(self.locale.ordinal_number(12), "12th") + self.assertEqual(self.locale.ordinal_number(13), "13th") + self.assertEqual(self.locale.ordinal_number(14), "14th") + self.assertEqual(self.locale.ordinal_number(21), "21st") + self.assertEqual(self.locale.ordinal_number(22), "22nd") + self.assertEqual(self.locale.ordinal_number(23), "23rd") + self.assertEqual(self.locale.ordinal_number(24), "24th") + + self.assertEqual(self.locale.ordinal_number(100), "100th") + self.assertEqual(self.locale.ordinal_number(101), "101st") + self.assertEqual(self.locale.ordinal_number(102), "102nd") + self.assertEqual(self.locale.ordinal_number(103), "103rd") + self.assertEqual(self.locale.ordinal_number(104), "104th") + self.assertEqual(self.locale.ordinal_number(110), "110th") + self.assertEqual(self.locale.ordinal_number(111), "111th") + self.assertEqual(self.locale.ordinal_number(112), "112th") + self.assertEqual(self.locale.ordinal_number(113), "113th") + self.assertEqual(self.locale.ordinal_number(114), "114th") + self.assertEqual(self.locale.ordinal_number(121), "121st") + self.assertEqual(self.locale.ordinal_number(122), "122nd") + self.assertEqual(self.locale.ordinal_number(123), "123rd") + self.assertEqual(self.locale.ordinal_number(124), "124th") def test_meridian_invalid_token(self): - assertEqual(self.locale.meridian(7, None), None) - assertEqual(self.locale.meridian(7, 'B'), None) - assertEqual(self.locale.meridian(7, 'NONSENSE'), None) + self.assertEqual(self.locale.meridian(7, None), None) + self.assertEqual(self.locale.meridian(7, "B"), None) + self.assertEqual(self.locale.meridian(7, "NONSENSE"), None) class EnglishLocaleTests(Chai): - def setUp(self): super(EnglishLocaleTests, self).setUp() self.locale = locales.EnglishLocale() def test_describe(self): - assertEqual(self.locale.describe('now', only_distance=True), 'instantly') - assertEqual(self.locale.describe('now', only_distance=False), 'just now') + self.assertEqual(self.locale.describe("now", only_distance=True), "instantly") + self.assertEqual(self.locale.describe("now", only_distance=False), "just now") class ItalianLocalesTests(Chai): - def test_ordinal_number(self): locale = locales.ItalianLocale() - assertEqual(locale.ordinal_number(1), '1º') + self.assertEqual(locale.ordinal_number(1), "1º") class SpanishLocalesTests(Chai): - def test_ordinal_number(self): locale = locales.SpanishLocale() - assertEqual(locale.ordinal_number(1), '1º') + self.assertEqual(locale.ordinal_number(1), "1º") class FrenchLocalesTests(Chai): - def test_ordinal_number(self): locale = locales.FrenchLocale() - assertEqual(locale.ordinal_number(1), '1er') - assertEqual(locale.ordinal_number(2), '2e') + self.assertEqual(locale.ordinal_number(1), "1er") + self.assertEqual(locale.ordinal_number(2), "2e") class RussianLocalesTests(Chai): - def test_plurals2(self): locale = locales.RussianLocale() - assertEqual(locale._format_timeframe('hours', 0), '0 часов') - assertEqual(locale._format_timeframe('hours', 1), '1 час') - assertEqual(locale._format_timeframe('hours', 2), '2 часа') - assertEqual(locale._format_timeframe('hours', 4), '4 часа') - assertEqual(locale._format_timeframe('hours', 5), '5 часов') - assertEqual(locale._format_timeframe('hours', 21), '21 час') - assertEqual(locale._format_timeframe('hours', 22), '22 часа') - assertEqual(locale._format_timeframe('hours', 25), '25 часов') + self.assertEqual(locale._format_timeframe("hours", 0), "0 часов") + self.assertEqual(locale._format_timeframe("hours", 1), "1 час") + self.assertEqual(locale._format_timeframe("hours", 2), "2 часа") + self.assertEqual(locale._format_timeframe("hours", 4), "4 часа") + self.assertEqual(locale._format_timeframe("hours", 5), "5 часов") + self.assertEqual(locale._format_timeframe("hours", 21), "21 час") + self.assertEqual(locale._format_timeframe("hours", 22), "22 часа") + self.assertEqual(locale._format_timeframe("hours", 25), "25 часов") # feminine grammatical gender should be tested separately - assertEqual(locale._format_timeframe('minutes', 0), '0 минут') - assertEqual(locale._format_timeframe('minutes', 1), '1 минуту') - assertEqual(locale._format_timeframe('minutes', 2), '2 минуты') - assertEqual(locale._format_timeframe('minutes', 4), '4 минуты') - assertEqual(locale._format_timeframe('minutes', 5), '5 минут') - assertEqual(locale._format_timeframe('minutes', 21), '21 минуту') - assertEqual(locale._format_timeframe('minutes', 22), '22 минуты') - assertEqual(locale._format_timeframe('minutes', 25), '25 минут') + self.assertEqual(locale._format_timeframe("minutes", 0), "0 минут") + self.assertEqual(locale._format_timeframe("minutes", 1), "1 минуту") + self.assertEqual(locale._format_timeframe("minutes", 2), "2 минуты") + self.assertEqual(locale._format_timeframe("minutes", 4), "4 минуты") + self.assertEqual(locale._format_timeframe("minutes", 5), "5 минут") + self.assertEqual(locale._format_timeframe("minutes", 21), "21 минуту") + self.assertEqual(locale._format_timeframe("minutes", 22), "22 минуты") + self.assertEqual(locale._format_timeframe("minutes", 25), "25 минут") class PolishLocalesTests(Chai): - def test_plurals(self): locale = locales.PolishLocale() - assertEqual(locale._format_timeframe('hours', 0), '0 godzin') - assertEqual(locale._format_timeframe('hours', 1), '1 godzin') - assertEqual(locale._format_timeframe('hours', 2), '2 godziny') - assertEqual(locale._format_timeframe('hours', 4), '4 godziny') - assertEqual(locale._format_timeframe('hours', 5), '5 godzin') - assertEqual(locale._format_timeframe('hours', 21), '21 godzin') - assertEqual(locale._format_timeframe('hours', 22), '22 godziny') - assertEqual(locale._format_timeframe('hours', 25), '25 godzin') + self.assertEqual(locale._format_timeframe("hours", 0), "0 godzin") + self.assertEqual(locale._format_timeframe("hours", 1), "1 godzin") + self.assertEqual(locale._format_timeframe("hours", 2), "2 godziny") + self.assertEqual(locale._format_timeframe("hours", 4), "4 godziny") + self.assertEqual(locale._format_timeframe("hours", 5), "5 godzin") + self.assertEqual(locale._format_timeframe("hours", 21), "21 godzin") + self.assertEqual(locale._format_timeframe("hours", 22), "22 godziny") + self.assertEqual(locale._format_timeframe("hours", 25), "25 godzin") class IcelandicLocalesTests(Chai): - def setUp(self): super(IcelandicLocalesTests, self).setUp() @@ -184,16 +172,15 @@ def setUp(self): def test_format_timeframe(self): - assertEqual(self.locale._format_timeframe('minute', -1), 'einni mínútu') - assertEqual(self.locale._format_timeframe('minute', 1), 'eina mínútu') + self.assertEqual(self.locale._format_timeframe("minute", -1), "einni mínútu") + self.assertEqual(self.locale._format_timeframe("minute", 1), "eina mínútu") - assertEqual(self.locale._format_timeframe('hours', -2), '2 tímum') - assertEqual(self.locale._format_timeframe('hours', 2), '2 tíma') - assertEqual(self.locale._format_timeframe('now', 0), 'rétt í þessu') + self.assertEqual(self.locale._format_timeframe("hours", -2), "2 tímum") + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 tíma") + self.assertEqual(self.locale._format_timeframe("now", 0), "rétt í þessu") class MalayalamLocaleTests(Chai): - def setUp(self): super(MalayalamLocaleTests, self).setUp() @@ -201,28 +188,27 @@ def setUp(self): def test_format_timeframe(self): - assertEqual(self.locale._format_timeframe('hours', 2), '2 മണിക്കൂർ') - assertEqual(self.locale._format_timeframe('hour', 0), 'ഒരു മണിക്കൂർ') + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 മണിക്കൂർ") + self.assertEqual(self.locale._format_timeframe("hour", 0), "ഒരു മണിക്കൂർ") def test_format_relative_now(self): - result = self.locale._format_relative('ഇപ്പോൾ', 'now', 0) + result = self.locale._format_relative("ഇപ്പോൾ", "now", 0) - assertEqual(result, 'ഇപ്പോൾ') + self.assertEqual(result, "ഇപ്പോൾ") def test_format_relative_past(self): - result = self.locale._format_relative('ഒരു മണിക്കൂർ', 'hour', 1) - assertEqual(result, 'ഒരു മണിക്കൂർ ശേഷം') + result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", 1) + self.assertEqual(result, "ഒരു മണിക്കൂർ ശേഷം") def test_format_relative_future(self): - result = self.locale._format_relative('ഒരു മണിക്കൂർ', 'hour', -1) - assertEqual(result, 'ഒരു മണിക്കൂർ മുമ്പ്') + result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", -1) + self.assertEqual(result, "ഒരു മണിക്കൂർ മുമ്പ്") class HindiLocaleTests(Chai): - def setUp(self): super(HindiLocaleTests, self).setUp() @@ -230,28 +216,27 @@ def setUp(self): def test_format_timeframe(self): - assertEqual(self.locale._format_timeframe('hours', 2), '2 घंटे') - assertEqual(self.locale._format_timeframe('hour', 0), 'एक घंटा') + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 घंटे") + self.assertEqual(self.locale._format_timeframe("hour", 0), "एक घंटा") def test_format_relative_now(self): - result = self.locale._format_relative('अभी', 'now', 0) + result = self.locale._format_relative("अभी", "now", 0) - assertEqual(result, 'अभी') + self.assertEqual(result, "अभी") def test_format_relative_past(self): - result = self.locale._format_relative('एक घंटा', 'hour', 1) - assertEqual(result, 'एक घंटा बाद') + result = self.locale._format_relative("एक घंटा", "hour", 1) + self.assertEqual(result, "एक घंटा बाद") def test_format_relative_future(self): - result = self.locale._format_relative('एक घंटा', 'hour', -1) - assertEqual(result, 'एक घंटा पहले') + result = self.locale._format_relative("एक घंटा", "hour", -1) + self.assertEqual(result, "एक घंटा पहले") class CzechLocaleTests(Chai): - def setUp(self): super(CzechLocaleTests, self).setUp() @@ -259,31 +244,30 @@ def setUp(self): def test_format_timeframe(self): - assertEqual(self.locale._format_timeframe('hours', 2), '2 hodiny') - assertEqual(self.locale._format_timeframe('hours', 5), '5 hodin') - assertEqual(self.locale._format_timeframe('hour', 0), '0 hodin') - assertEqual(self.locale._format_timeframe('hours', -2), '2 hodinami') - assertEqual(self.locale._format_timeframe('hours', -5), '5 hodinami') - assertEqual(self.locale._format_timeframe('now', 0), 'Teď') + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 hodiny") + self.assertEqual(self.locale._format_timeframe("hours", 5), "5 hodin") + self.assertEqual(self.locale._format_timeframe("hour", 0), "0 hodin") + self.assertEqual(self.locale._format_timeframe("hours", -2), "2 hodinami") + self.assertEqual(self.locale._format_timeframe("hours", -5), "5 hodinami") + self.assertEqual(self.locale._format_timeframe("now", 0), "Teď") def test_format_relative_now(self): - result = self.locale._format_relative('Teď', 'now', 0) - assertEqual(result, 'Teď') + result = self.locale._format_relative("Teď", "now", 0) + self.assertEqual(result, "Teď") def test_format_relative_future(self): - result = self.locale._format_relative('hodinu', 'hour', 1) - assertEqual(result, 'Za hodinu') + result = self.locale._format_relative("hodinu", "hour", 1) + self.assertEqual(result, "Za hodinu") def test_format_relative_past(self): - result = self.locale._format_relative('hodinou', 'hour', -1) - assertEqual(result, 'Před hodinou') + result = self.locale._format_relative("hodinou", "hour", -1) + self.assertEqual(result, "Před hodinou") class SlovakLocaleTests(Chai): - def setUp(self): super(SlovakLocaleTests, self).setUp() @@ -291,73 +275,70 @@ def setUp(self): def test_format_timeframe(self): - assertEqual(self.locale._format_timeframe('hours', 2), '2 hodiny') - assertEqual(self.locale._format_timeframe('hours', 5), '5 hodín') - assertEqual(self.locale._format_timeframe('hour', 0), '0 hodín') - assertEqual(self.locale._format_timeframe('hours', -2), '2 hodinami') - assertEqual(self.locale._format_timeframe('hours', -5), '5 hodinami') - assertEqual(self.locale._format_timeframe('now', 0), 'Teraz') + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 hodiny") + self.assertEqual(self.locale._format_timeframe("hours", 5), "5 hodín") + self.assertEqual(self.locale._format_timeframe("hour", 0), "0 hodín") + self.assertEqual(self.locale._format_timeframe("hours", -2), "2 hodinami") + self.assertEqual(self.locale._format_timeframe("hours", -5), "5 hodinami") + self.assertEqual(self.locale._format_timeframe("now", 0), "Teraz") def test_format_relative_now(self): - result = self.locale._format_relative('Teraz', 'now', 0) - assertEqual(result, 'Teraz') + result = self.locale._format_relative("Teraz", "now", 0) + self.assertEqual(result, "Teraz") def test_format_relative_future(self): - result = self.locale._format_relative('hodinu', 'hour', 1) - assertEqual(result, 'O hodinu') + result = self.locale._format_relative("hodinu", "hour", 1) + self.assertEqual(result, "O hodinu") def test_format_relative_past(self): - result = self.locale._format_relative('hodinou', 'hour', -1) - assertEqual(result, 'Pred hodinou') + result = self.locale._format_relative("hodinou", "hour", -1) + self.assertEqual(result, "Pred hodinou") class BulgarianLocaleTests(Chai): - def test_plurals2(self): locale = locales.BulgarianLocale() - assertEqual(locale._format_timeframe('hours', 0), '0 часа') - assertEqual(locale._format_timeframe('hours', 1), '1 час') - assertEqual(locale._format_timeframe('hours', 2), '2 часа') - assertEqual(locale._format_timeframe('hours', 4), '4 часа') - assertEqual(locale._format_timeframe('hours', 5), '5 часа') - assertEqual(locale._format_timeframe('hours', 21), '21 час') - assertEqual(locale._format_timeframe('hours', 22), '22 часа') - assertEqual(locale._format_timeframe('hours', 25), '25 часа') + self.assertEqual(locale._format_timeframe("hours", 0), "0 часа") + self.assertEqual(locale._format_timeframe("hours", 1), "1 час") + self.assertEqual(locale._format_timeframe("hours", 2), "2 часа") + self.assertEqual(locale._format_timeframe("hours", 4), "4 часа") + self.assertEqual(locale._format_timeframe("hours", 5), "5 часа") + self.assertEqual(locale._format_timeframe("hours", 21), "21 час") + self.assertEqual(locale._format_timeframe("hours", 22), "22 часа") + self.assertEqual(locale._format_timeframe("hours", 25), "25 часа") # feminine grammatical gender should be tested separately - assertEqual(locale._format_timeframe('minutes', 0), '0 минути') - assertEqual(locale._format_timeframe('minutes', 1), '1 минута') - assertEqual(locale._format_timeframe('minutes', 2), '2 минути') - assertEqual(locale._format_timeframe('minutes', 4), '4 минути') - assertEqual(locale._format_timeframe('minutes', 5), '5 минути') - assertEqual(locale._format_timeframe('minutes', 21), '21 минута') - assertEqual(locale._format_timeframe('minutes', 22), '22 минути') - assertEqual(locale._format_timeframe('minutes', 25), '25 минути') + self.assertEqual(locale._format_timeframe("minutes", 0), "0 минути") + self.assertEqual(locale._format_timeframe("minutes", 1), "1 минута") + self.assertEqual(locale._format_timeframe("minutes", 2), "2 минути") + self.assertEqual(locale._format_timeframe("minutes", 4), "4 минути") + self.assertEqual(locale._format_timeframe("minutes", 5), "5 минути") + self.assertEqual(locale._format_timeframe("minutes", 21), "21 минута") + self.assertEqual(locale._format_timeframe("minutes", 22), "22 минути") + self.assertEqual(locale._format_timeframe("minutes", 25), "25 минути") class HebrewLocaleTests(Chai): - def test_couple_of_timeframe(self): locale = locales.HebrewLocale() - assertEqual(locale._format_timeframe('hours', 2), 'שעתיים') - assertEqual(locale._format_timeframe('months', 2), 'חודשיים') - assertEqual(locale._format_timeframe('days', 2), 'יומיים') - assertEqual(locale._format_timeframe('years', 2), 'שנתיים') + self.assertEqual(locale._format_timeframe("hours", 2), "שעתיים") + self.assertEqual(locale._format_timeframe("months", 2), "חודשיים") + self.assertEqual(locale._format_timeframe("days", 2), "יומיים") + self.assertEqual(locale._format_timeframe("years", 2), "שנתיים") - assertEqual(locale._format_timeframe('hours', 3), '3 שעות') - assertEqual(locale._format_timeframe('months', 4), '4 חודשים') - assertEqual(locale._format_timeframe('days', 3), '3 ימים') - assertEqual(locale._format_timeframe('years', 5), '5 שנים') + self.assertEqual(locale._format_timeframe("hours", 3), "3 שעות") + self.assertEqual(locale._format_timeframe("months", 4), "4 חודשים") + self.assertEqual(locale._format_timeframe("days", 3), "3 ימים") + self.assertEqual(locale._format_timeframe("years", 5), "5 שנים") class MarathiLocaleTests(Chai): - def setUp(self): super(MarathiLocaleTests, self).setUp() @@ -365,146 +346,155 @@ def setUp(self): def test_dateCoreFunctionality(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) - assertEqual (self.locale.month_name(dt.month),'एप्रिल') - assertEqual (self.locale.month_abbreviation(dt.month),'एप्रि') - assertEqual (self.locale.day_name(dt.isoweekday()),'शनिवार') - assertEqual (self.locale.day_abbreviation(dt.isoweekday()), 'शनि') + self.assertEqual(self.locale.month_name(dt.month), "एप्रिल") + self.assertEqual(self.locale.month_abbreviation(dt.month), "एप्रि") + self.assertEqual(self.locale.day_name(dt.isoweekday()), "शनिवार") + self.assertEqual(self.locale.day_abbreviation(dt.isoweekday()), "शनि") def test_format_timeframe(self): - assertEqual(self.locale._format_timeframe('hours', 2), '2 तास') - assertEqual(self.locale._format_timeframe('hour', 0), 'एक तास') + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 तास") + self.assertEqual(self.locale._format_timeframe("hour", 0), "एक तास") def test_format_relative_now(self): - result = self.locale._format_relative('सद्य', 'now', 0) - assertEqual(result, 'सद्य') + result = self.locale._format_relative("सद्य", "now", 0) + self.assertEqual(result, "सद्य") def test_format_relative_past(self): - result = self.locale._format_relative('एक तास', 'hour', 1) - assertEqual(result, 'एक तास नंतर') + result = self.locale._format_relative("एक तास", "hour", 1) + self.assertEqual(result, "एक तास नंतर") def test_format_relative_future(self): - result = self.locale._format_relative('एक तास', 'hour', -1) - assertEqual(result, 'एक तास आधी') + result = self.locale._format_relative("एक तास", "hour", -1) + self.assertEqual(result, "एक तास आधी") # Not currently implemented def test_ordinal_number(self): - assertEqual(self.locale.ordinal_number(1), '1') + self.assertEqual(self.locale.ordinal_number(1), "1") class FinnishLocaleTests(Chai): - def setUp(self): super(FinnishLocaleTests, self).setUp() self.locale = locales.FinnishLocale() def test_format_timeframe(self): - assertEqual(self.locale._format_timeframe('hours', 2), - ('2 tuntia', '2 tunnin')) - assertEqual(self.locale._format_timeframe('hour', 0), - ('tunti', 'tunnin')) + self.assertEqual( + self.locale._format_timeframe("hours", 2), ("2 tuntia", "2 tunnin") + ) + self.assertEqual(self.locale._format_timeframe("hour", 0), ("tunti", "tunnin")) def test_format_relative_now(self): - result = self.locale._format_relative(['juuri nyt', 'juuri nyt'], 'now', 0) - assertEqual(result, 'juuri nyt') + result = self.locale._format_relative(["juuri nyt", "juuri nyt"], "now", 0) + self.assertEqual(result, "juuri nyt") def test_format_relative_past(self): - result = self.locale._format_relative(['tunti', 'tunnin'], 'hour', 1) - assertEqual(result, 'tunnin kuluttua') + result = self.locale._format_relative(["tunti", "tunnin"], "hour", 1) + self.assertEqual(result, "tunnin kuluttua") def test_format_relative_future(self): - result = self.locale._format_relative(['tunti', 'tunnin'], 'hour', -1) - assertEqual(result, 'tunti sitten') + result = self.locale._format_relative(["tunti", "tunnin"], "hour", -1) + self.assertEqual(result, "tunti sitten") def test_ordinal_number(self): - assertEqual(self.locale.ordinal_number(1), '1.') + self.assertEqual(self.locale.ordinal_number(1), "1.") class GermanLocaleTests(Chai): - def setUp(self): super(GermanLocaleTests, self).setUp() self.locale = locales.GermanLocale() def test_ordinal_number(self): - assertEqual(self.locale.ordinal_number(1), '1.') - + self.assertEqual(self.locale.ordinal_number(1), "1.") + def test_define(self): - assertEqual(self.locale.describe("minute", only_distance=True), 'eine Minute') - assertEqual(self.locale.describe("minute", only_distance=False), 'in einer Minute') - assertEqual(self.locale.describe("hour", only_distance=True), 'eine Stunde') - assertEqual(self.locale.describe("hour", only_distance=False), 'in einer Stunde') - assertEqual(self.locale.describe("day", only_distance=True), 'ein Tag') - assertEqual(self.locale.describe("day", only_distance=False), 'in einem Tag') - assertEqual(self.locale.describe("month", only_distance=True), 'ein Monat') - assertEqual(self.locale.describe("month", only_distance=False), 'in einem Monat') - assertEqual(self.locale.describe("year", only_distance=True), 'ein Jahr') - assertEqual(self.locale.describe("year", only_distance=False), 'in einem Jahr') + self.assertEqual( + self.locale.describe("minute", only_distance=True), "eine Minute" + ) + self.assertEqual( + self.locale.describe("minute", only_distance=False), "in einer Minute" + ) + self.assertEqual( + self.locale.describe("hour", only_distance=True), "eine Stunde" + ) + self.assertEqual( + self.locale.describe("hour", only_distance=False), "in einer Stunde" + ) + self.assertEqual(self.locale.describe("day", only_distance=True), "ein Tag") + self.assertEqual( + self.locale.describe("day", only_distance=False), "in einem Tag" + ) + self.assertEqual(self.locale.describe("month", only_distance=True), "ein Monat") + self.assertEqual( + self.locale.describe("month", only_distance=False), "in einem Monat" + ) + self.assertEqual(self.locale.describe("year", only_distance=True), "ein Jahr") + self.assertEqual( + self.locale.describe("year", only_distance=False), "in einem Jahr" + ) class HungarianLocaleTests(Chai): - def setUp(self): super(HungarianLocaleTests, self).setUp() self.locale = locales.HungarianLocale() def test_format_timeframe(self): - assertEqual(self.locale._format_timeframe('hours', 2), '2 óra') - assertEqual(self.locale._format_timeframe('hour', 0), 'egy órával') - assertEqual(self.locale._format_timeframe('hours', -2), '2 órával') - assertEqual(self.locale._format_timeframe('now', 0), 'éppen most') + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 óra") + self.assertEqual(self.locale._format_timeframe("hour", 0), "egy órával") + self.assertEqual(self.locale._format_timeframe("hours", -2), "2 órával") + self.assertEqual(self.locale._format_timeframe("now", 0), "éppen most") class EsperantoLocaleTests(Chai): - def setUp(self): super(EsperantoLocaleTests, self).setUp() self.locale = locales.EsperantoLocale() def test_format_timeframe(self): - assertEqual(self.locale._format_timeframe('hours', 2), '2 horoj') - assertEqual(self.locale._format_timeframe('hour', 0), 'un horo') - assertEqual(self.locale._format_timeframe('hours', -2), '2 horoj') - assertEqual(self.locale._format_timeframe('now', 0), 'nun') + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 horoj") + self.assertEqual(self.locale._format_timeframe("hour", 0), "un horo") + self.assertEqual(self.locale._format_timeframe("hours", -2), "2 horoj") + self.assertEqual(self.locale._format_timeframe("now", 0), "nun") def test_ordinal_number(self): - assertEqual(self.locale.ordinal_number(1), '1a') + self.assertEqual(self.locale.ordinal_number(1), "1a") -class ThaiLocaleTests(Chai): +class ThaiLocaleTests(Chai): def setUp(self): super(ThaiLocaleTests, self).setUp() self.locale = locales.ThaiLocale() def test_year_full(self): - assertEqual(self.locale.year_full(2015), '2558') + self.assertEqual(self.locale.year_full(2015), "2558") def test_year_abbreviation(self): - assertEqual(self.locale.year_abbreviation(2015), '58') + self.assertEqual(self.locale.year_abbreviation(2015), "58") def test_format_relative_now(self): - result = self.locale._format_relative('ขณะนี้', 'now', 0) - assertEqual(result, 'ขณะนี้') + result = self.locale._format_relative("ขณะนี้", "now", 0) + self.assertEqual(result, "ขณะนี้") def test_format_relative_past(self): - result = self.locale._format_relative('1 ชั่วโมง', 'hour', 1) - assertEqual(result, 'ในอีก 1 ชั่วโมง') - result = self.locale._format_relative('{0} ชั่วโมง', 'hours', 2) - assertEqual(result, 'ในอีก {0} ชั่วโมง') - result = self.locale._format_relative('ไม่กี่วินาที', 'seconds', 42) - assertEqual(result, 'ในอีกไม่กี่วินาที') + result = self.locale._format_relative("1 ชั่วโมง", "hour", 1) + self.assertEqual(result, "ในอีก 1 ชั่วโมง") + result = self.locale._format_relative("{0} ชั่วโมง", "hours", 2) + self.assertEqual(result, "ในอีก {0} ชั่วโมง") + result = self.locale._format_relative("ไม่กี่วินาที", "seconds", 42) + self.assertEqual(result, "ในอีกไม่กี่วินาที") def test_format_relative_future(self): - result = self.locale._format_relative('1 ชั่วโมง', 'hour', -1) - assertEqual(result, '1 ชั่วโมง ที่ผ่านมา') + result = self.locale._format_relative("1 ชั่วโมง", "hour", -1) + self.assertEqual(result, "1 ชั่วโมง ที่ผ่านมา") class BengaliLocaleTests(Chai): - def setUp(self): super(BengaliLocaleTests, self).setUp() @@ -520,20 +510,19 @@ def test_ordinal_number(self): result10 = self.locale._ordinal_number(10) result11 = self.locale._ordinal_number(11) result42 = self.locale._ordinal_number(42) - assertEqual(result0, '0তম') - assertEqual(result1, '1ম') - assertEqual(result3, '3য়') - assertEqual(result4, '4র্থ') - assertEqual(result5, '5ম') - assertEqual(result6, '6ষ্ঠ') - assertEqual(result10, '10ম') - assertEqual(result11, '11তম') - assertEqual(result42, '42তম') - assertEqual(self.locale._ordinal_number(-1), None) + self.assertEqual(result0, "0তম") + self.assertEqual(result1, "1ম") + self.assertEqual(result3, "3য়") + self.assertEqual(result4, "4র্থ") + self.assertEqual(result5, "5ম") + self.assertEqual(result6, "6ষ্ঠ") + self.assertEqual(result10, "10ম") + self.assertEqual(result11, "11তম") + self.assertEqual(result42, "42তম") + self.assertEqual(self.locale._ordinal_number(-1), None) class SwissLocaleTests(Chai): - def setUp(self): super(SwissLocaleTests, self).setUp() @@ -542,13 +531,12 @@ def setUp(self): def test_ordinal_number(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) - assertEqual(self.locale._format_timeframe('minute', 1), 'einer Minute') - assertEqual(self.locale._format_timeframe('hour', 1), 'einer Stunde') - assertEqual(self.locale.day_abbreviation(dt.isoweekday()), 'Sa') + self.assertEqual(self.locale._format_timeframe("minute", 1), "einer Minute") + self.assertEqual(self.locale._format_timeframe("hour", 1), "einer Stunde") + self.assertEqual(self.locale.day_abbreviation(dt.isoweekday()), "Sa") class RomanianLocaleTests(Chai): - def setUp(self): super(RomanianLocaleTests, self).setUp() @@ -556,26 +544,42 @@ def setUp(self): def test_timeframes(self): - self.assertEqual(self.locale._format_timeframe('hours', 2), '2 ore') - self.assertEqual(self.locale._format_timeframe('months', 2), '2 luni') + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 ore") + self.assertEqual(self.locale._format_timeframe("months", 2), "2 luni") - self.assertEqual(self.locale._format_timeframe('days', 2), '2 zile') - self.assertEqual(self.locale._format_timeframe('years', 2), '2 ani') + self.assertEqual(self.locale._format_timeframe("days", 2), "2 zile") + self.assertEqual(self.locale._format_timeframe("years", 2), "2 ani") - self.assertEqual(self.locale._format_timeframe('hours', 3), '3 ore') - self.assertEqual(self.locale._format_timeframe('months', 4), '4 luni') - self.assertEqual(self.locale._format_timeframe('days', 3), '3 zile') - self.assertEqual(self.locale._format_timeframe('years', 5), '5 ani') + self.assertEqual(self.locale._format_timeframe("hours", 3), "3 ore") + self.assertEqual(self.locale._format_timeframe("months", 4), "4 luni") + self.assertEqual(self.locale._format_timeframe("days", 3), "3 zile") + self.assertEqual(self.locale._format_timeframe("years", 5), "5 ani") def test_relative_timeframes(self): self.assertEqual(self.locale._format_relative("acum", "now", 0), "acum") - self.assertEqual(self.locale._format_relative("o oră", "hour", 1), "peste o oră") - self.assertEqual(self.locale._format_relative("o oră", "hour", -1), "o oră în urmă") - self.assertEqual(self.locale._format_relative("un minut", "minute", 1), "peste un minut") - self.assertEqual(self.locale._format_relative("un minut", "minute", -1), "un minut în urmă") - self.assertEqual(self.locale._format_relative("câteva secunde", "seconds", -1), "câteva secunde în urmă") - self.assertEqual(self.locale._format_relative("câteva secunde", "seconds", 1), "peste câteva secunde") - self.assertEqual(self.locale._format_relative("o zi", "day", -1), "o zi în urmă") + self.assertEqual( + self.locale._format_relative("o oră", "hour", 1), "peste o oră" + ) + self.assertEqual( + self.locale._format_relative("o oră", "hour", -1), "o oră în urmă" + ) + self.assertEqual( + self.locale._format_relative("un minut", "minute", 1), "peste un minut" + ) + self.assertEqual( + self.locale._format_relative("un minut", "minute", -1), "un minut în urmă" + ) + self.assertEqual( + self.locale._format_relative("câteva secunde", "seconds", -1), + "câteva secunde în urmă", + ) + self.assertEqual( + self.locale._format_relative("câteva secunde", "seconds", 1), + "peste câteva secunde", + ) + self.assertEqual( + self.locale._format_relative("o zi", "day", -1), "o zi în urmă" + ) self.assertEqual(self.locale._format_relative("o zi", "day", 1), "peste o zi") @@ -588,90 +592,95 @@ def setUp(self): def test_timeframes(self): # single - self.assertEqual(self.locale._format_timeframe('minute', 1), 'دقيقة') - self.assertEqual(self.locale._format_timeframe('hour', 1), 'ساعة') - self.assertEqual(self.locale._format_timeframe('day', 1), 'يوم') - self.assertEqual(self.locale._format_timeframe('month', 1), 'شهر') - self.assertEqual(self.locale._format_timeframe('year', 1), 'سنة') + self.assertEqual(self.locale._format_timeframe("minute", 1), "دقيقة") + self.assertEqual(self.locale._format_timeframe("hour", 1), "ساعة") + self.assertEqual(self.locale._format_timeframe("day", 1), "يوم") + self.assertEqual(self.locale._format_timeframe("month", 1), "شهر") + self.assertEqual(self.locale._format_timeframe("year", 1), "سنة") # double - self.assertEqual(self.locale._format_timeframe('minutes', 2), 'دقيقتين') - self.assertEqual(self.locale._format_timeframe('hours', 2), 'ساعتين') - self.assertEqual(self.locale._format_timeframe('days', 2), 'يومين') - self.assertEqual(self.locale._format_timeframe('months', 2), 'شهرين') - self.assertEqual(self.locale._format_timeframe('years', 2), 'سنتين') + self.assertEqual(self.locale._format_timeframe("minutes", 2), "دقيقتين") + self.assertEqual(self.locale._format_timeframe("hours", 2), "ساعتين") + self.assertEqual(self.locale._format_timeframe("days", 2), "يومين") + self.assertEqual(self.locale._format_timeframe("months", 2), "شهرين") + self.assertEqual(self.locale._format_timeframe("years", 2), "سنتين") # up to ten - self.assertEqual(self.locale._format_timeframe('minutes', 3), '3 دقائق') - self.assertEqual(self.locale._format_timeframe('hours', 4), '4 ساعات') - self.assertEqual(self.locale._format_timeframe('days', 5), '5 أيام') - self.assertEqual(self.locale._format_timeframe('months', 6), '6 أشهر') - self.assertEqual(self.locale._format_timeframe('years', 10), '10 سنوات') + self.assertEqual(self.locale._format_timeframe("minutes", 3), "3 دقائق") + self.assertEqual(self.locale._format_timeframe("hours", 4), "4 ساعات") + self.assertEqual(self.locale._format_timeframe("days", 5), "5 أيام") + self.assertEqual(self.locale._format_timeframe("months", 6), "6 أشهر") + self.assertEqual(self.locale._format_timeframe("years", 10), "10 سنوات") # more than ten - self.assertEqual(self.locale._format_timeframe('minutes', 11), '11 دقيقة') - self.assertEqual(self.locale._format_timeframe('hours', 19), '19 ساعة') - self.assertEqual(self.locale._format_timeframe('months', 24), '24 شهر') - self.assertEqual(self.locale._format_timeframe('days', 50), '50 يوم') - self.assertEqual(self.locale._format_timeframe('years', 115), '115 سنة') + self.assertEqual(self.locale._format_timeframe("minutes", 11), "11 دقيقة") + self.assertEqual(self.locale._format_timeframe("hours", 19), "19 ساعة") + self.assertEqual(self.locale._format_timeframe("months", 24), "24 شهر") + self.assertEqual(self.locale._format_timeframe("days", 50), "50 يوم") + self.assertEqual(self.locale._format_timeframe("years", 115), "115 سنة") class NepaliLocaleTests(Chai): - def setUp(self): super(NepaliLocaleTests, self).setUp() self.locale = locales.NepaliLocale() def test_format_timeframe(self): - assertEqual(self.locale._format_timeframe('hours', 3), '3 घण्टा') - assertEqual(self.locale._format_timeframe('hour', 0), 'एक घण्टा') + self.assertEqual(self.locale._format_timeframe("hours", 3), "3 घण्टा") + self.assertEqual(self.locale._format_timeframe("hour", 0), "एक घण्टा") def test_format_relative_now(self): - result = self.locale._format_relative('अहिले', 'now', 0) - assertEqual(result, 'अहिले') + result = self.locale._format_relative("अहिले", "now", 0) + self.assertEqual(result, "अहिले") def test_format_relative_future(self): - result = self.locale._format_relative('एक घण्टा', 'hour', 1) - assertEqual(result, 'एक घण्टा पछी') + result = self.locale._format_relative("एक घण्टा", "hour", 1) + self.assertEqual(result, "एक घण्टा पछी") def test_format_relative_past(self): - result = self.locale._format_relative('एक घण्टा', 'hour', -1) - assertEqual(result, 'एक घण्टा पहिले') + result = self.locale._format_relative("एक घण्टा", "hour", -1) + self.assertEqual(result, "एक घण्टा पहिले") class IndonesianLocaleTests(Chai): - def setUp(self): super(IndonesianLocaleTests, self).setUp() self.locale = locales.IndonesianLocale() def test_timeframes(self): - self.assertEqual(self.locale._format_timeframe('hours', 2), '2 jam') - self.assertEqual(self.locale._format_timeframe('months', 2), '2 bulan') + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 jam") + self.assertEqual(self.locale._format_timeframe("months", 2), "2 bulan") - self.assertEqual(self.locale._format_timeframe('days', 2), '2 hari') - self.assertEqual(self.locale._format_timeframe('years', 2), '2 tahun') + self.assertEqual(self.locale._format_timeframe("days", 2), "2 hari") + self.assertEqual(self.locale._format_timeframe("years", 2), "2 tahun") - self.assertEqual(self.locale._format_timeframe('hours', 3), '3 jam') - self.assertEqual(self.locale._format_timeframe('months', 4), '4 bulan') - self.assertEqual(self.locale._format_timeframe('days', 3), '3 hari') - self.assertEqual(self.locale._format_timeframe('years', 5), '5 tahun') + self.assertEqual(self.locale._format_timeframe("hours", 3), "3 jam") + self.assertEqual(self.locale._format_timeframe("months", 4), "4 bulan") + self.assertEqual(self.locale._format_timeframe("days", 3), "3 hari") + self.assertEqual(self.locale._format_timeframe("years", 5), "5 tahun") def test_format_relative_now(self): - self.assertEqual(self.locale._format_relative('baru saja', 'now', 0), 'baru saja') + self.assertEqual( + self.locale._format_relative("baru saja", "now", 0), "baru saja" + ) def test_format_relative_past(self): - self.assertEqual(self.locale._format_relative('1 jam', 'hour', 1), 'dalam 1 jam') - self.assertEqual(self.locale._format_relative('1 detik', 'seconds', 1), 'dalam 1 detik') + self.assertEqual( + self.locale._format_relative("1 jam", "hour", 1), "dalam 1 jam" + ) + self.assertEqual( + self.locale._format_relative("1 detik", "seconds", 1), "dalam 1 detik" + ) def test_format_relative_future(self): - self.assertEqual(self.locale._format_relative('1 jam', 'hour', -1), '1 jam yang lalu') + self.assertEqual( + self.locale._format_relative("1 jam", "hour", -1), "1 jam yang lalu" + ) class TagalogLocaleTests(Chai): - def setUp(self): super(TagalogLocaleTests, self).setUp() @@ -679,82 +688,87 @@ def setUp(self): def test_format_timeframe(self): - assertEqual(self.locale._format_timeframe('minute', 1), 'isang minuto') - assertEqual(self.locale._format_timeframe('hour', 1), 'isang oras') - assertEqual(self.locale._format_timeframe('month', 1), 'isang buwan') - assertEqual(self.locale._format_timeframe('year', 1), 'isang taon') + self.assertEqual(self.locale._format_timeframe("minute", 1), "isang minuto") + self.assertEqual(self.locale._format_timeframe("hour", 1), "isang oras") + self.assertEqual(self.locale._format_timeframe("month", 1), "isang buwan") + self.assertEqual(self.locale._format_timeframe("year", 1), "isang taon") - assertEqual(self.locale._format_timeframe('seconds', 2), 'segundo') - assertEqual(self.locale._format_timeframe('minutes', 3), '3 minuto') - assertEqual(self.locale._format_timeframe('hours', 4), '4 oras') - assertEqual(self.locale._format_timeframe('months', 5), '5 buwan') - assertEqual(self.locale._format_timeframe('years', 6), '6 taon') + self.assertEqual(self.locale._format_timeframe("seconds", 2), "segundo") + self.assertEqual(self.locale._format_timeframe("minutes", 3), "3 minuto") + self.assertEqual(self.locale._format_timeframe("hours", 4), "4 oras") + self.assertEqual(self.locale._format_timeframe("months", 5), "5 buwan") + self.assertEqual(self.locale._format_timeframe("years", 6), "6 taon") def test_format_relative_now(self): - self.assertEqual(self.locale._format_relative('ngayon lang', 'now', 0), 'ngayon lang') + self.assertEqual( + self.locale._format_relative("ngayon lang", "now", 0), "ngayon lang" + ) def test_format_relative_past(self): - self.assertEqual(self.locale._format_relative('2 oras', 'hour', 2), '2 oras mula ngayon') + self.assertEqual( + self.locale._format_relative("2 oras", "hour", 2), "2 oras mula ngayon" + ) def test_format_relative_future(self): - self.assertEqual(self.locale._format_relative('3 oras', 'hour', -3), 'nakaraang 3 oras') + self.assertEqual( + self.locale._format_relative("3 oras", "hour", -3), "nakaraang 3 oras" + ) def test_ordinal_number(self): - assertEqual(self.locale.ordinal_number(0), 'ika-0') - assertEqual(self.locale.ordinal_number(1), 'ika-1') - assertEqual(self.locale.ordinal_number(2), 'ika-2') - assertEqual(self.locale.ordinal_number(3), 'ika-3') - assertEqual(self.locale.ordinal_number(10), 'ika-10') - assertEqual(self.locale.ordinal_number(23), 'ika-23') - assertEqual(self.locale.ordinal_number(100), 'ika-100') - assertEqual(self.locale.ordinal_number(103), 'ika-103') - assertEqual(self.locale.ordinal_number(114), 'ika-114') - - -class EstonianLocaleTests(Chai): + self.assertEqual(self.locale.ordinal_number(0), "ika-0") + self.assertEqual(self.locale.ordinal_number(1), "ika-1") + self.assertEqual(self.locale.ordinal_number(2), "ika-2") + self.assertEqual(self.locale.ordinal_number(3), "ika-3") + self.assertEqual(self.locale.ordinal_number(10), "ika-10") + self.assertEqual(self.locale.ordinal_number(23), "ika-23") + self.assertEqual(self.locale.ordinal_number(100), "ika-100") + self.assertEqual(self.locale.ordinal_number(103), "ika-103") + self.assertEqual(self.locale.ordinal_number(114), "ika-114") + +class EstonianLocaleTests(Chai): def setUp(self): super(EstonianLocaleTests, self).setUp() self.locale = locales.EstonianLocale() def test_format_timeframe(self): - assertEqual(self.locale._format_timeframe('now', 0), 'just nüüd') - assertEqual(self.locale._format_timeframe('second', 1), 'ühe sekundi') - assertEqual(self.locale._format_timeframe('seconds', 3), '3 sekundi') - assertEqual(self.locale._format_timeframe('seconds', 30), '30 sekundi') - assertEqual(self.locale._format_timeframe('minute', 1), 'ühe minuti') - assertEqual(self.locale._format_timeframe('minutes', 4), '4 minuti') - assertEqual(self.locale._format_timeframe('minutes', 40), '40 minuti') - assertEqual(self.locale._format_timeframe('hour', 1), 'tunni aja') - assertEqual(self.locale._format_timeframe('hours', 5), '5 tunni') - assertEqual(self.locale._format_timeframe('hours', 23), '23 tunni') - assertEqual(self.locale._format_timeframe('day', 1), 'ühe päeva') - assertEqual(self.locale._format_timeframe('days', 6), '6 päeva') - assertEqual(self.locale._format_timeframe('days', 12), '12 päeva') - assertEqual(self.locale._format_timeframe('month', 1), 'ühe kuu') - assertEqual(self.locale._format_timeframe('months', 7), '7 kuu') - assertEqual(self.locale._format_timeframe('months', 11), '11 kuu') - assertEqual(self.locale._format_timeframe('year', 1), 'ühe aasta') - assertEqual(self.locale._format_timeframe('years', 8), '8 aasta') - assertEqual(self.locale._format_timeframe('years', 12), '12 aasta') - - assertEqual(self.locale._format_timeframe('now', 0), 'just nüüd') - assertEqual(self.locale._format_timeframe('second', -1), 'üks sekund') - assertEqual(self.locale._format_timeframe('seconds', -9), '9 sekundit') - assertEqual(self.locale._format_timeframe('seconds', -12), '12 sekundit') - assertEqual(self.locale._format_timeframe('minute', -1), 'üks minut') - assertEqual(self.locale._format_timeframe('minutes', -2), '2 minutit') - assertEqual(self.locale._format_timeframe('minutes', -10), '10 minutit') - assertEqual(self.locale._format_timeframe('hour', -1), 'tund aega') - assertEqual(self.locale._format_timeframe('hours', -3), '3 tundi') - assertEqual(self.locale._format_timeframe('hours', -11), '11 tundi') - assertEqual(self.locale._format_timeframe('day', -1), 'üks päev') - assertEqual(self.locale._format_timeframe('days', -2), '2 päeva') - assertEqual(self.locale._format_timeframe('days', -12), '12 päeva') - assertEqual(self.locale._format_timeframe('month', -1), 'üks kuu') - assertEqual(self.locale._format_timeframe('months', -3), '3 kuud') - assertEqual(self.locale._format_timeframe('months', -13), '13 kuud') - assertEqual(self.locale._format_timeframe('year', -1), 'üks aasta') - assertEqual(self.locale._format_timeframe('years', -4), '4 aastat') - assertEqual(self.locale._format_timeframe('years', -14), '14 aastat') + self.assertEqual(self.locale._format_timeframe("now", 0), "just nüüd") + self.assertEqual(self.locale._format_timeframe("second", 1), "ühe sekundi") + self.assertEqual(self.locale._format_timeframe("seconds", 3), "3 sekundi") + self.assertEqual(self.locale._format_timeframe("seconds", 30), "30 sekundi") + self.assertEqual(self.locale._format_timeframe("minute", 1), "ühe minuti") + self.assertEqual(self.locale._format_timeframe("minutes", 4), "4 minuti") + self.assertEqual(self.locale._format_timeframe("minutes", 40), "40 minuti") + self.assertEqual(self.locale._format_timeframe("hour", 1), "tunni aja") + self.assertEqual(self.locale._format_timeframe("hours", 5), "5 tunni") + self.assertEqual(self.locale._format_timeframe("hours", 23), "23 tunni") + self.assertEqual(self.locale._format_timeframe("day", 1), "ühe päeva") + self.assertEqual(self.locale._format_timeframe("days", 6), "6 päeva") + self.assertEqual(self.locale._format_timeframe("days", 12), "12 päeva") + self.assertEqual(self.locale._format_timeframe("month", 1), "ühe kuu") + self.assertEqual(self.locale._format_timeframe("months", 7), "7 kuu") + self.assertEqual(self.locale._format_timeframe("months", 11), "11 kuu") + self.assertEqual(self.locale._format_timeframe("year", 1), "ühe aasta") + self.assertEqual(self.locale._format_timeframe("years", 8), "8 aasta") + self.assertEqual(self.locale._format_timeframe("years", 12), "12 aasta") + + self.assertEqual(self.locale._format_timeframe("now", 0), "just nüüd") + self.assertEqual(self.locale._format_timeframe("second", -1), "üks sekund") + self.assertEqual(self.locale._format_timeframe("seconds", -9), "9 sekundit") + self.assertEqual(self.locale._format_timeframe("seconds", -12), "12 sekundit") + self.assertEqual(self.locale._format_timeframe("minute", -1), "üks minut") + self.assertEqual(self.locale._format_timeframe("minutes", -2), "2 minutit") + self.assertEqual(self.locale._format_timeframe("minutes", -10), "10 minutit") + self.assertEqual(self.locale._format_timeframe("hour", -1), "tund aega") + self.assertEqual(self.locale._format_timeframe("hours", -3), "3 tundi") + self.assertEqual(self.locale._format_timeframe("hours", -11), "11 tundi") + self.assertEqual(self.locale._format_timeframe("day", -1), "üks päev") + self.assertEqual(self.locale._format_timeframe("days", -2), "2 päeva") + self.assertEqual(self.locale._format_timeframe("days", -12), "12 päeva") + self.assertEqual(self.locale._format_timeframe("month", -1), "üks kuu") + self.assertEqual(self.locale._format_timeframe("months", -3), "3 kuud") + self.assertEqual(self.locale._format_timeframe("months", -13), "13 kuud") + self.assertEqual(self.locale._format_timeframe("year", -1), "üks aasta") + self.assertEqual(self.locale._format_timeframe("years", -4), "4 aastat") + self.assertEqual(self.locale._format_timeframe("years", -14), "14 aastat") diff --git a/tests/parser_tests.py b/tests/parser_tests.py index a179be4e6..a212f0426 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from chai import Chai -from datetime import datetime -from dateutil import tz import calendar import time +from datetime import datetime + +from chai import Chai +from dateutil import tz from arrow import parser from arrow.parser import DateTimeParser, ParserError class DateTimeParserTests(Chai): - def setUp(self): super(DateTimeParserTests, self).setUp() @@ -20,87 +20,99 @@ def setUp(self): def test_parse_multiformat(self): - mock_datetime = mock() + mock_datetime = self.mock() - expect(self.parser.parse).args('str', 'fmt_a').raises(ParserError) - expect(self.parser.parse).args('str', 'fmt_b').returns(mock_datetime) + self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserError) + self.expect(self.parser.parse).args("str", "fmt_b").returns(mock_datetime) - result = self.parser._parse_multiformat('str', ['fmt_a', 'fmt_b']) + result = self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) - assertEqual(result, mock_datetime) + self.assertEqual(result, mock_datetime) def test_parse_multiformat_all_fail(self): - expect(self.parser.parse).args('str', 'fmt_a').raises(ParserError) - expect(self.parser.parse).args('str', 'fmt_b').raises(ParserError) - - with assertRaises(ParserError): - self.parser._parse_multiformat('str', ['fmt_a', 'fmt_b']) + self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserError) + self.expect(self.parser.parse).args("str", "fmt_b").raises(ParserError) - def test_parse_multiformat_unexpected_fail(self): + with self.assertRaises(ParserError): + self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) - class UnexpectedError(Exception): + def test_parse_multiformat_unself_expected_fail(self): + class UnselfExpectedError(Exception): pass - expect(self.parser.parse).args('str', 'fmt_a').raises(UnexpectedError) + self.expect(self.parser.parse).args("str", "fmt_a").raises(UnselfExpectedError) - with assertRaises(UnexpectedError): - self.parser._parse_multiformat('str', ['fmt_a', 'fmt_b']) + with self.assertRaises(UnselfExpectedError): + self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) def test_parse_token_nonsense(self): parts = {} - self.parser._parse_token('NONSENSE', '1900', parts) - assertEqual(parts, {}) + self.parser._parse_token("NONSENSE", "1900", parts) + self.assertEqual(parts, {}) def test_parse_token_invalid_meridians(self): parts = {} - self.parser._parse_token('A', 'a..m', parts) - assertEqual(parts, {}) - self.parser._parse_token('a', 'p..m', parts) - assertEqual(parts, {}) + self.parser._parse_token("A", "a..m", parts) + self.assertEqual(parts, {}) + self.parser._parse_token("a", "p..m", parts) + self.assertEqual(parts, {}) def test_parser_no_caching(self): - expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_a').times(100) + self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( + 100 + ) self.parser = parser.DateTimeParser(cache_size=0) for _ in range(100): - self.parser._generate_pattern_re('fmt_a') + self.parser._generate_pattern_re("fmt_a") def test_parser_1_line_caching(self): - expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_a').times(1) + self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( + 1 + ) self.parser = parser.DateTimeParser(cache_size=1) for _ in range(100): - self.parser._generate_pattern_re('fmt_a') + self.parser._generate_pattern_re("fmt_a") - expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_b').times(1) + self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_b").times( + 1 + ) for _ in range(100): - self.parser._generate_pattern_re('fmt_a') - self.parser._generate_pattern_re('fmt_b') + self.parser._generate_pattern_re("fmt_a") + self.parser._generate_pattern_re("fmt_b") - expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_a').times(1) + self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( + 1 + ) for _ in range(100): - self.parser._generate_pattern_re('fmt_a') + self.parser._generate_pattern_re("fmt_a") def test_parser_multiple_line_caching(self): - expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_a').times(1) + self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( + 1 + ) self.parser = parser.DateTimeParser(cache_size=2) for _ in range(100): - self.parser._generate_pattern_re('fmt_a') + self.parser._generate_pattern_re("fmt_a") - expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_b').times(1) + self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_b").times( + 1 + ) for _ in range(100): - self.parser._generate_pattern_re('fmt_a') - self.parser._generate_pattern_re('fmt_b') + self.parser._generate_pattern_re("fmt_a") + self.parser._generate_pattern_re("fmt_b") - expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_a').times(0) + self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( + 0 + ) for _ in range(100): - self.parser._generate_pattern_re('fmt_a') + self.parser._generate_pattern_re("fmt_a") class DateTimeParserParseTests(Chai): - def setUp(self): super(DateTimeParserParseTests, self).setUp() @@ -108,82 +120,93 @@ def setUp(self): def test_parse_list(self): - expect(self.parser._parse_multiformat).args('str', ['fmt_a', 'fmt_b']).returns('result') + self.expect(self.parser._parse_multiformat).args( + "str", ["fmt_a", "fmt_b"] + ).returns("result") - result = self.parser.parse('str', ['fmt_a', 'fmt_b']) + result = self.parser.parse("str", ["fmt_a", "fmt_b"]) - assertEqual(result, 'result') + self.assertEqual(result, "result") def test_parse_unrecognized_token(self): - mock_input_re_map = mock(self.parser, '_input_re_map') + mock_input_re_map = self.mock(self.parser, "_input_re_map") - expect(mock_input_re_map.__getitem__).args('YYYY').raises(KeyError) + self.expect(mock_input_re_map.__getitem__).args("YYYY").raises(KeyError) - with assertRaises(parser.ParserError): - self.parser.parse('2013-01-01', 'YYYY-MM-DD') + with self.assertRaises(parser.ParserError): + self.parser.parse("2013-01-01", "YYYY-MM-DD") def test_parse_parse_no_match(self): - with assertRaises(parser.ParserError): - self.parser.parse('01-01', 'YYYY-MM-DD') + with self.assertRaises(parser.ParserError): + self.parser.parse("01-01", "YYYY-MM-DD") def test_parse_separators(self): - with assertRaises(parser.ParserError): - self.parser.parse('1403549231', 'YYYY-MM-DD') + with self.assertRaises(parser.ParserError): + self.parser.parse("1403549231", "YYYY-MM-DD") def test_parse_numbers(self): - expected = datetime(2012, 1, 1, 12, 5, 10) - assertEqual(self.parser.parse('2012-01-01 12:05:10', 'YYYY-MM-DD HH:mm:ss'), expected) + self.expected = datetime(2012, 1, 1, 12, 5, 10) + self.assertEqual( + self.parser.parse("2012-01-01 12:05:10", "YYYY-MM-DD HH:mm:ss"), + self.expected, + ) def test_parse_year_two_digit(self): - expected = datetime(1979, 1, 1, 12, 5, 10) - assertEqual(self.parser.parse('79-01-01 12:05:10', 'YY-MM-DD HH:mm:ss'), expected) + self.expected = datetime(1979, 1, 1, 12, 5, 10) + self.assertEqual( + 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() timestamp = int(time.time()) - expected = datetime.fromtimestamp(timestamp, tz=tz_utc) - assertEqual(self.parser.parse(str(timestamp), 'X'), expected) + self.expected = datetime.fromtimestamp(timestamp, tz=tz_utc) + self.assertEqual(self.parser.parse(str(timestamp), "X"), self.expected) def test_parse_names(self): - expected = datetime(2012, 1, 1) + self.expected = datetime(2012, 1, 1) - assertEqual(self.parser.parse('January 1, 2012', 'MMMM D, YYYY'), expected) - assertEqual(self.parser.parse('Jan 1, 2012', 'MMM D, YYYY'), expected) + self.assertEqual( + self.parser.parse("January 1, 2012", "MMMM D, YYYY"), self.expected + ) + self.assertEqual(self.parser.parse("Jan 1, 2012", "MMM D, YYYY"), self.expected) def test_parse_pm(self): - expected = datetime(1, 1, 1, 13, 0, 0) - assertEqual(self.parser.parse('1 pm', 'H a'), expected) - assertEqual(self.parser.parse('1 pm', 'h a'), expected) + self.expected = datetime(1, 1, 1, 13, 0, 0) + self.assertEqual(self.parser.parse("1 pm", "H a"), self.expected) + self.assertEqual(self.parser.parse("1 pm", "h a"), self.expected) - expected = datetime(1, 1, 1, 1, 0, 0) - assertEqual(self.parser.parse('1 am', 'H A'), expected) - assertEqual(self.parser.parse('1 am', 'h A'), expected) + self.expected = datetime(1, 1, 1, 1, 0, 0) + self.assertEqual(self.parser.parse("1 am", "H A"), self.expected) + self.assertEqual(self.parser.parse("1 am", "h A"), self.expected) - expected = datetime(1, 1, 1, 0, 0, 0) - assertEqual(self.parser.parse('12 am', 'H A'), expected) - assertEqual(self.parser.parse('12 am', 'h A'), expected) + self.expected = datetime(1, 1, 1, 0, 0, 0) + self.assertEqual(self.parser.parse("12 am", "H A"), self.expected) + self.assertEqual(self.parser.parse("12 am", "h A"), self.expected) - expected = datetime(1, 1, 1, 12, 0, 0) - assertEqual(self.parser.parse('12 pm', 'H A'), expected) - assertEqual(self.parser.parse('12 pm', 'h A'), expected) + self.expected = datetime(1, 1, 1, 12, 0, 0) + self.assertEqual(self.parser.parse("12 pm", "H A"), self.expected) + self.assertEqual(self.parser.parse("12 pm", "h A"), self.expected) def test_parse_tz_hours_only(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') - assertEqual(parsed, expected) + 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") + self.assertEqual(parsed, self.expected) def test_parse_tz_zz(self): - expected = datetime(2013, 1, 1, tzinfo=tz.tzoffset(None, -7 * 3600)) - assertEqual(self.parser.parse('2013-01-01 -07:00', 'YYYY-MM-DD ZZ'), expected) + self.expected = datetime(2013, 1, 1, tzinfo=tz.tzoffset(None, -7 * 3600)) + self.assertEqual( + self.parser.parse("2013-01-01 -07:00", "YYYY-MM-DD ZZ"), self.expected + ) def test_parse_tz_name_zzz(self): for tz_name in ( @@ -191,90 +214,120 @@ def test_parse_tz_name_zzz(self): # the tz database but it is actualy tricky to retrieve them from # dateutil so here is short list that should match all # naming patterns/conventions in used tz databaze - 'Africa/Tripoli', - 'America/Port_of_Spain', - 'Australia/LHI', - 'Etc/GMT-11', - 'Etc/GMT0', - 'Etc/UCT', - 'Etc/GMT+9', - 'GMT+0', - 'CST6CDT', - 'GMT-0', - 'W-SU', + "Africa/Tripoli", + "America/Port_of_Spain", + "Australia/LHI", + "Etc/GMT-11", + "Etc/GMT0", + "Etc/UCT", + "Etc/GMT+9", + "GMT+0", + "CST6CDT", + "GMT-0", + "W-SU", ): - expected = datetime(2013, 1, 1, tzinfo=tz.gettz(tz_name)) - assertEqual(self.parser.parse('2013-01-01 %s' % tz_name, 'YYYY-MM-DD ZZZ'), expected) + self.expected = datetime(2013, 1, 1, tzinfo=tz.gettz(tz_name)) + self.assertEqual( + self.parser.parse("2013-01-01 %s" % tz_name, "YYYY-MM-DD ZZZ"), + self.expected, + ) # note that offsets are not timezones - with assertRaises(ParserError): - self.parser.parse('2013-01-01 +1000', 'YYYY-MM-DD ZZZ') + with self.assertRaises(ParserError): + self.parser.parse("2013-01-01 +1000", "YYYY-MM-DD ZZZ") def test_parse_subsecond(self): - expected = datetime(2013, 1, 1, 12, 30, 45, 900000) - assertEqual(self.parser.parse('2013-01-01 12:30:45.9', 'YYYY-MM-DD HH:mm:ss.S'), expected) - assertEqual(self.parser.parse_iso('2013-01-01 12:30:45.9'), expected) + self.expected = datetime(2013, 1, 1, 12, 30, 45, 900000) + self.assertEqual( + self.parser.parse("2013-01-01 12:30:45.9", "YYYY-MM-DD HH:mm:ss.S"), + self.expected, + ) + self.assertEqual(self.parser.parse_iso("2013-01-01 12:30:45.9"), self.expected) - expected = datetime(2013, 1, 1, 12, 30, 45, 980000) - assertEqual(self.parser.parse('2013-01-01 12:30:45.98', 'YYYY-MM-DD HH:mm:ss.SS'), expected) - assertEqual(self.parser.parse_iso('2013-01-01 12:30:45.98'), expected) + self.expected = datetime(2013, 1, 1, 12, 30, 45, 980000) + self.assertEqual( + self.parser.parse("2013-01-01 12:30:45.98", "YYYY-MM-DD HH:mm:ss.SS"), + self.expected, + ) + self.assertEqual(self.parser.parse_iso("2013-01-01 12:30:45.98"), self.expected) - expected = datetime(2013, 1, 1, 12, 30, 45, 987000) - assertEqual(self.parser.parse('2013-01-01 12:30:45.987', 'YYYY-MM-DD HH:mm:ss.SSS'), expected) - assertEqual(self.parser.parse_iso('2013-01-01 12:30:45.987'), expected) + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987000) + self.assertEqual( + self.parser.parse("2013-01-01 12:30:45.987", "YYYY-MM-DD HH:mm:ss.SSS"), + self.expected, + ) + self.assertEqual( + self.parser.parse_iso("2013-01-01 12:30:45.987"), self.expected + ) - expected = datetime(2013, 1, 1, 12, 30, 45, 987600) - assertEqual(self.parser.parse('2013-01-01 12:30:45.9876', 'YYYY-MM-DD HH:mm:ss.SSSS'), expected) - assertEqual(self.parser.parse_iso('2013-01-01 12:30:45.9876'), expected) + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987600) + self.assertEqual( + self.parser.parse("2013-01-01 12:30:45.9876", "YYYY-MM-DD HH:mm:ss.SSSS"), + self.expected, + ) + self.assertEqual( + self.parser.parse_iso("2013-01-01 12:30:45.9876"), self.expected + ) - expected = datetime(2013, 1, 1, 12, 30, 45, 987650) - assertEqual(self.parser.parse('2013-01-01 12:30:45.98765', 'YYYY-MM-DD HH:mm:ss.SSSSS'), expected) - assertEqual(self.parser.parse_iso('2013-01-01 12:30:45.98765'), expected) + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987650) + self.assertEqual( + self.parser.parse("2013-01-01 12:30:45.98765", "YYYY-MM-DD HH:mm:ss.SSSSS"), + self.expected, + ) + self.assertEqual( + self.parser.parse_iso("2013-01-01 12:30:45.98765"), self.expected + ) - expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - assertEqual(self.parser.parse('2013-01-01 12:30:45.987654', 'YYYY-MM-DD HH:mm:ss.SSSSSS'), expected) - assertEqual(self.parser.parse_iso('2013-01-01 12:30:45.987654'), expected) + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) + self.assertEqual( + self.parser.parse( + "2013-01-01 12:30:45.987654", "YYYY-MM-DD HH:mm:ss.SSSSSS" + ), + self.expected, + ) + self.assertEqual( + self.parser.parse_iso("2013-01-01 12:30:45.987654"), self.expected + ) def test_parse_subsecond_rounding(self): - expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - format = 'YYYY-MM-DD HH:mm:ss.S' + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) + format = "YYYY-MM-DD HH:mm:ss.S" # round up - string = '2013-01-01 12:30:45.9876539' - assertEqual(self.parser.parse(string, format), expected) - assertEqual(self.parser.parse_iso(string), expected) + string = "2013-01-01 12:30:45.9876539" + self.assertEqual(self.parser.parse(string, format), self.expected) + self.assertEqual(self.parser.parse_iso(string), self.expected) # round down - string = '2013-01-01 12:30:45.98765432' - assertEqual(self.parser.parse(string, format), expected) - #import pudb; pudb.set_trace() - assertEqual(self.parser.parse_iso(string), expected) + string = "2013-01-01 12:30:45.98765432" + self.assertEqual(self.parser.parse(string, format), self.expected) + # import pudb; pudb.set_trace() + self.assertEqual(self.parser.parse_iso(string), self.expected) # round half-up - string = '2013-01-01 12:30:45.987653521' - assertEqual(self.parser.parse(string, format), expected) - assertEqual(self.parser.parse_iso(string), expected) + string = "2013-01-01 12:30:45.987653521" + self.assertEqual(self.parser.parse(string, format), self.expected) + self.assertEqual(self.parser.parse_iso(string), self.expected) # round half-down - string = '2013-01-01 12:30:45.9876545210' - assertEqual(self.parser.parse(string, format), expected) - assertEqual(self.parser.parse_iso(string), expected) + string = "2013-01-01 12:30:45.9876545210" + self.assertEqual(self.parser.parse(string, format), self.expected) + self.assertEqual(self.parser.parse_iso(string), self.expected) def test_map_lookup_keyerror(self): - with assertRaises(parser.ParserError): - parser.DateTimeParser._map_lookup({'a': '1'}, 'b') + with self.assertRaises(parser.ParserError): + parser.DateTimeParser._map_lookup({"a": "1"}, "b") def test_try_timestamp(self): - assertEqual(parser.DateTimeParser._try_timestamp('1.1'), 1.1) - assertEqual(parser.DateTimeParser._try_timestamp('1'), 1) - assertEqual(parser.DateTimeParser._try_timestamp('abc'), None) + self.assertEqual(parser.DateTimeParser._try_timestamp("1.1"), 1.1) + self.assertEqual(parser.DateTimeParser._try_timestamp("1"), 1) + self.assertEqual(parser.DateTimeParser._try_timestamp("abc"), None) class DateTimeParserRegexTests(Chai): - def setUp(self): super(DateTimeParserRegexTests, self).setUp() @@ -282,229 +335,233 @@ def setUp(self): def test_format_year(self): - assertEqual(self.format_regex.findall('YYYY-YY'), ['YYYY', 'YY']) + self.assertEqual(self.format_regex.findall("YYYY-YY"), ["YYYY", "YY"]) def test_format_month(self): - assertEqual(self.format_regex.findall('MMMM-MMM-MM-M'), ['MMMM', 'MMM', 'MM', 'M']) + self.assertEqual( + self.format_regex.findall("MMMM-MMM-MM-M"), ["MMMM", "MMM", "MM", "M"] + ) def test_format_day(self): - assertEqual(self.format_regex.findall('DDDD-DDD-DD-D'), ['DDDD', 'DDD', 'DD', 'D']) + self.assertEqual( + self.format_regex.findall("DDDD-DDD-DD-D"), ["DDDD", "DDD", "DD", "D"] + ) def test_format_hour(self): - assertEqual(self.format_regex.findall('HH-H-hh-h'), ['HH', 'H', 'hh', 'h']) + self.assertEqual(self.format_regex.findall("HH-H-hh-h"), ["HH", "H", "hh", "h"]) def test_format_minute(self): - assertEqual(self.format_regex.findall('mm-m'), ['mm', 'm']) + self.assertEqual(self.format_regex.findall("mm-m"), ["mm", "m"]) def test_format_second(self): - assertEqual(self.format_regex.findall('ss-s'), ['ss', 's']) + self.assertEqual(self.format_regex.findall("ss-s"), ["ss", "s"]) def test_format_subsecond(self): - assertEqual(self.format_regex.findall('SSSSSS-SSSSS-SSSS-SSS-SS-S'), - ['SSSSSS', 'SSSSS', 'SSSS', 'SSS', 'SS', 'S']) + self.assertEqual( + self.format_regex.findall("SSSSSS-SSSSS-SSSS-SSS-SS-S"), + ["SSSSSS", "SSSSS", "SSSS", "SSS", "SS", "S"], + ) def test_format_tz(self): - assertEqual(self.format_regex.findall('ZZ-Z'), ['ZZ', 'Z']) + self.assertEqual(self.format_regex.findall("ZZ-Z"), ["ZZ", "Z"]) def test_format_am_pm(self): - assertEqual(self.format_regex.findall('A-a'), ['A', 'a']) + self.assertEqual(self.format_regex.findall("A-a"), ["A", "a"]) def test_format_timestamp(self): - assertEqual(self.format_regex.findall('X'), ['X']) + self.assertEqual(self.format_regex.findall("X"), ["X"]) def test_month_names(self): - p = parser.DateTimeParser('en_us') + p = parser.DateTimeParser("en_us") - text = '_'.join(calendar.month_name[1:]) + text = "_".join(calendar.month_name[1:]) - result = p._input_re_map['MMMM'].findall(text) + result = p._input_re_map["MMMM"].findall(text) - assertEqual(result, calendar.month_name[1:]) + self.assertEqual(result, calendar.month_name[1:]) def test_month_abbreviations(self): - p = parser.DateTimeParser('en_us') + p = parser.DateTimeParser("en_us") - text = '_'.join(calendar.month_abbr[1:]) + text = "_".join(calendar.month_abbr[1:]) - result = p._input_re_map['MMM'].findall(text) + result = p._input_re_map["MMM"].findall(text) - assertEqual(result, calendar.month_abbr[1:]) + self.assertEqual(result, calendar.month_abbr[1:]) def test_digits(self): - assertEqual(parser.DateTimeParser._TWO_DIGIT_RE.findall('12-3-45'), ['12', '45']) - assertEqual(parser.DateTimeParser._FOUR_DIGIT_RE.findall('1234-56'), ['1234']) - assertEqual(parser.DateTimeParser._ONE_OR_TWO_DIGIT_RE.findall('4-56'), ['4', '56']) + self.assertEqual( + parser.DateTimeParser._TWO_DIGIT_RE.findall("12-3-45"), ["12", "45"] + ) + self.assertEqual( + parser.DateTimeParser._FOUR_DIGIT_RE.findall("1234-56"), ["1234"] + ) + self.assertEqual( + parser.DateTimeParser._ONE_OR_TWO_DIGIT_RE.findall("4-56"), ["4", "56"] + ) class DateTimeParserISOTests(Chai): - def setUp(self): super(DateTimeParserISOTests, self).setUp() - self.parser = parser.DateTimeParser('en_us') + self.parser = parser.DateTimeParser("en_us") def test_YYYY(self): - assertEqual( - self.parser.parse_iso('2013'), - datetime(2013, 1, 1) - ) + self.assertEqual(self.parser.parse_iso("2013"), datetime(2013, 1, 1)) def test_YYYY_MM(self): for separator in DateTimeParser.SEPARATORS: - assertEqual( - self.parser.parse_iso(separator.join(('2013', '02'))), - datetime(2013, 2, 1) + self.assertEqual( + self.parser.parse_iso(separator.join(("2013", "02"))), + datetime(2013, 2, 1), ) def test_YYYY_MM_DD(self): for separator in DateTimeParser.SEPARATORS: - assertEqual( - self.parser.parse_iso(separator.join(('2013', '02', '03'))), - datetime(2013, 2, 3) + self.assertEqual( + self.parser.parse_iso(separator.join(("2013", "02", "03"))), + datetime(2013, 2, 3), ) def test_YYYY_MM_DDTHH_mmZ(self): - assertEqual( - self.parser.parse_iso('2013-02-03T04:05+01:00'), - datetime(2013, 2, 3, 4, 5, tzinfo=tz.tzoffset(None, 3600)) + self.assertEqual( + 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): - assertEqual( - self.parser.parse_iso('2013-02-03T04:05'), - datetime(2013, 2, 3, 4, 5) + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05"), datetime(2013, 2, 3, 4, 5) ) def test_YYYY_MM_DDTHH_mm_ssZ(self): - assertEqual( - self.parser.parse_iso('2013-02-03T04:05:06+01:00'), - datetime(2013, 2, 3, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600)) + self.assertEqual( + 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): - assertEqual( - self.parser.parse_iso('2013-02-03T04:05:06'), - datetime(2013, 2, 3, 4, 5, 6) + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06"), datetime(2013, 2, 3, 4, 5, 6) ) def test_YYYY_MM_DD_HH_mmZ(self): - assertEqual( - self.parser.parse_iso('2013-02-03 04:05+01:00'), - datetime(2013, 2, 3, 4, 5, tzinfo=tz.tzoffset(None, 3600)) + self.assertEqual( + 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): - assertEqual( - self.parser.parse_iso('2013-02-03 04:05'), - datetime(2013, 2, 3, 4, 5) + self.assertEqual( + self.parser.parse_iso("2013-02-03 04:05"), datetime(2013, 2, 3, 4, 5) ) def test_YYYY_MM_DD_HH_mm_ssZ(self): - assertEqual( - self.parser.parse_iso('2013-02-03 04:05:06+01:00'), - datetime(2013, 2, 3, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600)) + self.assertEqual( + 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): - assertEqual( - self.parser.parse_iso('2013-02-03 04:05:06'), - datetime(2013, 2, 3, 4, 5, 6) + self.assertEqual( + 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): - assertEqual( - self.parser.parse_iso('2013-02-03T04:05:06.7'), - datetime(2013, 2, 3, 4, 5, 6, 700000) + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.7"), + datetime(2013, 2, 3, 4, 5, 6, 700000), ) - assertEqual( - self.parser.parse_iso('2013-02-03T04:05:06.78'), - datetime(2013, 2, 3, 4, 5, 6, 780000) + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.78"), + datetime(2013, 2, 3, 4, 5, 6, 780000), ) - assertEqual( - self.parser.parse_iso('2013-02-03T04:05:06.789'), - datetime(2013, 2, 3, 4, 5, 6, 789000) + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.789"), + datetime(2013, 2, 3, 4, 5, 6, 789000), ) - assertEqual( - self.parser.parse_iso('2013-02-03T04:05:06.7891'), - datetime(2013, 2, 3, 4, 5, 6, 789100) + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.7891"), + datetime(2013, 2, 3, 4, 5, 6, 789100), ) - assertEqual( - self.parser.parse_iso('2013-02-03T04:05:06.78912'), - datetime(2013, 2, 3, 4, 5, 6, 789120) + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.78912"), + datetime(2013, 2, 3, 4, 5, 6, 789120), ) # ISO 8601:2004(E), ISO, 2004-12-01, 4.2.2.4 ... the decimal fraction # shall be divided from the integer part by the decimal sign specified # in ISO 31-0, i.e. the comma [,] or full stop [.]. Of these, the comma # is the preferred sign. - assertEqual( - self.parser.parse_iso('2013-02-03T04:05:06,789123678'), - datetime(2013, 2, 3, 4, 5, 6, 789124) + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06,789123678"), + datetime(2013, 2, 3, 4, 5, 6, 789124), ) # there is no limit on the number of decimal places - assertEqual( - self.parser.parse_iso('2013-02-03T04:05:06.789123678'), - datetime(2013, 2, 3, 4, 5, 6, 789124) + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.789123678"), + datetime(2013, 2, 3, 4, 5, 6, 789124), ) def test_YYYY_MM_DDTHH_mm_ss_SZ(self): - assertEqual( - 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)) + self.assertEqual( + 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)), ) - assertEqual( - self.parser.parse_iso('2013-02-03T04:05:06.78+01:00'), - datetime(2013, 2, 3, 4, 5, 6, 780000, tzinfo=tz.tzoffset(None, 3600)) + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.78+01:00"), + datetime(2013, 2, 3, 4, 5, 6, 780000, tzinfo=tz.tzoffset(None, 3600)), ) - assertEqual( - self.parser.parse_iso('2013-02-03T04:05:06.789+01:00'), - datetime(2013, 2, 3, 4, 5, 6, 789000, tzinfo=tz.tzoffset(None, 3600)) + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.789+01:00"), + datetime(2013, 2, 3, 4, 5, 6, 789000, tzinfo=tz.tzoffset(None, 3600)), ) - assertEqual( - self.parser.parse_iso('2013-02-03T04:05:06.7891+01:00'), - datetime(2013, 2, 3, 4, 5, 6, 789100, tzinfo=tz.tzoffset(None, 3600)) + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.7891+01:00"), + datetime(2013, 2, 3, 4, 5, 6, 789100, tzinfo=tz.tzoffset(None, 3600)), ) - assertEqual( - self.parser.parse_iso('2013-02-03T04:05:06.78912+01:00'), - datetime(2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzoffset(None, 3600)) + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.78912+01:00"), + datetime(2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzoffset(None, 3600)), ) # Properly parse string with Z timezone - assertEqual( - self.parser.parse_iso('2013-02-03T04:05:06.78912Z'), - datetime(2013, 2, 3, 4, 5, 6, 789120) + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.78912Z"), + datetime(2013, 2, 3, 4, 5, 6, 789120), ) def test_gnu_date(self): @@ -512,32 +569,29 @@ def test_gnu_date(self): regression tests for parsing output from GNU date(1) """ # date -Ins - assertEqual( - self.parser.parse_iso('2016-11-16T09:46:30,895636557-0800'), + self.assertEqual( + self.parser.parse_iso("2016-11-16T09:46:30,895636557-0800"), datetime( - 2016, 11, 16, 9, 46, 30, 895636, - tzinfo=tz.tzoffset(None, -3600 * 8), - ) + 2016, 11, 16, 9, 46, 30, 895636, tzinfo=tz.tzoffset(None, -3600 * 8) + ), ) # date --rfc-3339=ns - assertEqual( - self.parser.parse_iso('2016-11-16 09:51:14.682141526-08:00'), + self.assertEqual( + self.parser.parse_iso("2016-11-16 09:51:14.682141526-08:00"), datetime( - 2016, 11, 16, 9, 51, 14, 682142, - tzinfo=tz.tzoffset(None, -3600 * 8), - ) + 2016, 11, 16, 9, 51, 14, 682142, tzinfo=tz.tzoffset(None, -3600 * 8) + ), ) def test_isoformat(self): dt = datetime.utcnow() - assertEqual(self.parser.parse_iso(dt.isoformat()), dt) + self.assertEqual(self.parser.parse_iso(dt.isoformat()), dt) class TzinfoParserTests(Chai): - def setUp(self): super(TzinfoParserTests, self).setUp() @@ -545,300 +599,298 @@ def setUp(self): def test_parse_local(self): - assertEqual(self.parser.parse('local'), tz.tzlocal()) + self.assertEqual(self.parser.parse("local"), tz.tzlocal()) def test_parse_utc(self): - assertEqual(self.parser.parse('utc'), tz.tzutc()) - assertEqual(self.parser.parse('UTC'), tz.tzutc()) + self.assertEqual(self.parser.parse("utc"), tz.tzutc()) + self.assertEqual(self.parser.parse("UTC"), tz.tzutc()) def test_parse_iso(self): - assertEqual(self.parser.parse('01:00'), tz.tzoffset(None, 3600)) - assertEqual(self.parser.parse('+01:00'), tz.tzoffset(None, 3600)) - assertEqual(self.parser.parse('-01:00'), tz.tzoffset(None, -3600)) + self.assertEqual(self.parser.parse("01:00"), tz.tzoffset(None, 3600)) + self.assertEqual(self.parser.parse("+01:00"), tz.tzoffset(None, 3600)) + self.assertEqual(self.parser.parse("-01:00"), tz.tzoffset(None, -3600)) def test_parse_str(self): - assertEqual(self.parser.parse('US/Pacific'), tz.gettz('US/Pacific')) + self.assertEqual(self.parser.parse("US/Pacific"), tz.gettz("US/Pacific")) def test_parse_fails(self): - with assertRaises(parser.ParserError): - self.parser.parse('fail') + with self.assertRaises(parser.ParserError): + self.parser.parse("fail") class DateTimeParserMonthNameTests(Chai): - def setUp(self): super(DateTimeParserMonthNameTests, self).setUp() - self.parser = parser.DateTimeParser('en_us') + self.parser = parser.DateTimeParser("en_us") def test_shortmonth_capitalized(self): - assertEqual( - self.parser.parse('2013-Jan-01', 'YYYY-MMM-DD'), - datetime(2013, 1, 1) + self.assertEqual( + self.parser.parse("2013-Jan-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) ) def test_shortmonth_allupper(self): - assertEqual( - self.parser.parse('2013-JAN-01', 'YYYY-MMM-DD'), - datetime(2013, 1, 1) + self.assertEqual( + self.parser.parse("2013-JAN-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) ) def test_shortmonth_alllower(self): - assertEqual( - self.parser.parse('2013-jan-01', 'YYYY-MMM-DD'), - datetime(2013, 1, 1) + self.assertEqual( + self.parser.parse("2013-jan-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) ) def test_month_capitalized(self): - assertEqual( - self.parser.parse('2013-January-01', 'YYYY-MMMM-DD'), - datetime(2013, 1, 1) + self.assertEqual( + self.parser.parse("2013-January-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) ) def test_month_allupper(self): - assertEqual( - self.parser.parse('2013-JANUARY-01', 'YYYY-MMMM-DD'), - datetime(2013, 1, 1) + self.assertEqual( + self.parser.parse("2013-JANUARY-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) ) def test_month_alllower(self): - assertEqual( - self.parser.parse('2013-january-01', 'YYYY-MMMM-DD'), - datetime(2013, 1, 1) + self.assertEqual( + self.parser.parse("2013-january-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) ) def test_localized_month_name(self): - parser_ = parser.DateTimeParser('fr_fr') + parser_ = parser.DateTimeParser("fr_fr") - assertEqual( - parser_.parse('2013-Janvier-01', 'YYYY-MMMM-DD'), - datetime(2013, 1, 1) + self.assertEqual( + parser_.parse("2013-Janvier-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) ) def test_localized_month_abbreviation(self): - parser_ = parser.DateTimeParser('it_it') + parser_ = parser.DateTimeParser("it_it") - assertEqual( - parser_.parse('2013-Gen-01', 'YYYY-MMM-DD'), - datetime(2013, 1, 1) + self.assertEqual( + parser_.parse("2013-Gen-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) ) class DateTimeParserMeridiansTests(Chai): - def setUp(self): super(DateTimeParserMeridiansTests, self).setUp() - self.parser = parser.DateTimeParser('en_us') + self.parser = parser.DateTimeParser("en_us") def test_meridians_lowercase(self): - assertEqual( - self.parser.parse('2013-01-01 5am', 'YYYY-MM-DD ha'), - datetime(2013, 1, 1, 5) + self.assertEqual( + self.parser.parse("2013-01-01 5am", "YYYY-MM-DD ha"), + datetime(2013, 1, 1, 5), ) - assertEqual( - self.parser.parse('2013-01-01 5pm', 'YYYY-MM-DD ha'), - datetime(2013, 1, 1, 17) + self.assertEqual( + self.parser.parse("2013-01-01 5pm", "YYYY-MM-DD ha"), + datetime(2013, 1, 1, 17), ) def test_meridians_capitalized(self): - assertEqual( - self.parser.parse('2013-01-01 5AM', 'YYYY-MM-DD hA'), - datetime(2013, 1, 1, 5) + self.assertEqual( + self.parser.parse("2013-01-01 5AM", "YYYY-MM-DD hA"), + datetime(2013, 1, 1, 5), ) - assertEqual( - self.parser.parse('2013-01-01 5PM', 'YYYY-MM-DD hA'), - datetime(2013, 1, 1, 17) + self.assertEqual( + self.parser.parse("2013-01-01 5PM", "YYYY-MM-DD hA"), + datetime(2013, 1, 1, 17), ) def test_localized_meridians_lowercase(self): - parser_ = parser.DateTimeParser('hu_hu') - assertEqual( - parser_.parse('2013-01-01 5 de', 'YYYY-MM-DD h a'), - datetime(2013, 1, 1, 5) + parser_ = parser.DateTimeParser("hu_hu") + self.assertEqual( + parser_.parse("2013-01-01 5 de", "YYYY-MM-DD h a"), datetime(2013, 1, 1, 5) ) - assertEqual( - parser_.parse('2013-01-01 5 du', 'YYYY-MM-DD h a'), - datetime(2013, 1, 1, 17) + self.assertEqual( + parser_.parse("2013-01-01 5 du", "YYYY-MM-DD h a"), datetime(2013, 1, 1, 17) ) def test_localized_meridians_capitalized(self): - parser_ = parser.DateTimeParser('hu_hu') - assertEqual( - parser_.parse('2013-01-01 5 DE', 'YYYY-MM-DD h A'), - datetime(2013, 1, 1, 5) + parser_ = parser.DateTimeParser("hu_hu") + self.assertEqual( + parser_.parse("2013-01-01 5 DE", "YYYY-MM-DD h A"), datetime(2013, 1, 1, 5) ) - assertEqual( - parser_.parse('2013-01-01 5 DU', 'YYYY-MM-DD h A'), - datetime(2013, 1, 1, 17) + self.assertEqual( + parser_.parse("2013-01-01 5 DU", "YYYY-MM-DD h A"), datetime(2013, 1, 1, 17) ) class DateTimeParserMonthOrdinalDayTests(Chai): - def setUp(self): super(DateTimeParserMonthOrdinalDayTests, self).setUp() - self.parser = parser.DateTimeParser('en_us') + self.parser = parser.DateTimeParser("en_us") def test_english(self): - parser_ = parser.DateTimeParser('en_us') + parser_ = parser.DateTimeParser("en_us") - assertEqual( - parser_.parse('January 1st, 2013', 'MMMM Do, YYYY'), - datetime(2013, 1, 1) + self.assertEqual( + parser_.parse("January 1st, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) ) - assertEqual( - parser_.parse('January 2nd, 2013', 'MMMM Do, YYYY'), - datetime(2013, 1, 2) + self.assertEqual( + parser_.parse("January 2nd, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 2) ) - assertEqual( - parser_.parse('January 3rd, 2013', 'MMMM Do, YYYY'), - datetime(2013, 1, 3) + self.assertEqual( + parser_.parse("January 3rd, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 3) ) - assertEqual( - parser_.parse('January 4th, 2013', 'MMMM Do, YYYY'), - datetime(2013, 1, 4) + self.assertEqual( + parser_.parse("January 4th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 4) ) - assertEqual( - parser_.parse('January 11th, 2013', 'MMMM Do, YYYY'), - datetime(2013, 1, 11) + self.assertEqual( + parser_.parse("January 11th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 11) ) - assertEqual( - parser_.parse('January 12th, 2013', 'MMMM Do, YYYY'), - datetime(2013, 1, 12) + self.assertEqual( + parser_.parse("January 12th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 12) ) - assertEqual( - parser_.parse('January 13th, 2013', 'MMMM Do, YYYY'), - datetime(2013, 1, 13) + self.assertEqual( + parser_.parse("January 13th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 13) ) - assertEqual( - parser_.parse('January 21st, 2013', 'MMMM Do, YYYY'), - datetime(2013, 1, 21) + self.assertEqual( + parser_.parse("January 21st, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 21) ) - assertEqual( - parser_.parse('January 31st, 2013', 'MMMM Do, YYYY'), - datetime(2013, 1, 31) + self.assertEqual( + parser_.parse("January 31st, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 31) ) - with assertRaises(ParserError): - parser_.parse('January 1th, 2013', 'MMMM Do, YYYY') + with self.assertRaises(ParserError): + parser_.parse("January 1th, 2013", "MMMM Do, YYYY") - with assertRaises(ParserError): - parser_.parse('January 11st, 2013', 'MMMM Do, YYYY') + with self.assertRaises(ParserError): + parser_.parse("January 11st, 2013", "MMMM Do, YYYY") def test_italian(self): - parser_ = parser.DateTimeParser('it_it') + parser_ = parser.DateTimeParser("it_it") - assertEqual(parser_.parse('Gennaio 1º, 2013', 'MMMM Do, YYYY'), - datetime(2013, 1, 1)) + self.assertEqual( + parser_.parse("Gennaio 1º, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) + ) def test_spanish(self): - parser_ = parser.DateTimeParser('es_es') + parser_ = parser.DateTimeParser("es_es") - assertEqual(parser_.parse('Enero 1º, 2013', 'MMMM Do, YYYY'), - datetime(2013, 1, 1)) + self.assertEqual( + parser_.parse("Enero 1º, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) + ) def test_french(self): - parser_ = parser.DateTimeParser('fr_fr') + parser_ = parser.DateTimeParser("fr_fr") - assertEqual(parser_.parse('Janvier 1er, 2013', 'MMMM Do, YYYY'), - datetime(2013, 1, 1)) + self.assertEqual( + parser_.parse("Janvier 1er, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) + ) - assertEqual(parser_.parse('Janvier 2e, 2013', 'MMMM Do, YYYY'), - datetime(2013, 1, 2)) + self.assertEqual( + parser_.parse("Janvier 2e, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 2) + ) - assertEqual(parser_.parse('Janvier 11e, 2013', 'MMMM Do, YYYY'), - datetime(2013, 1, 11)) + self.assertEqual( + parser_.parse("Janvier 11e, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 11) + ) class DateTimeParserSearchDateTests(Chai): - def setUp(self): super(DateTimeParserSearchDateTests, self).setUp() self.parser = parser.DateTimeParser() def test_parse_search(self): - assertEqual( - self.parser.parse('Today is 25 of September of 2003', 'DD of MMMM of YYYY'), - datetime(2003, 9, 25)) + self.assertEqual( + 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): - assertEqual( - 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)) + self.assertEqual( + 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), + ) - assertEqual( - self.parser.parse('Call 01-02-03 on 79-01-01 12:05:10', 'YY-MM-DD HH:mm:ss'), - datetime(1979, 1, 1, 12, 5, 10)) + self.assertEqual( + self.parser.parse( + "Call 01-02-03 on 79-01-01 12:05:10", "YY-MM-DD HH:mm:ss" + ), + datetime(1979, 1, 1, 12, 5, 10), + ) def test_parse_search_with_names(self): - assertEqual( - self.parser.parse('June was born in May 1980', 'MMMM YYYY'), - datetime(1980, 5, 1)) + self.assertEqual( + self.parser.parse("June was born in May 1980", "MMMM YYYY"), + datetime(1980, 5, 1), + ) def test_parse_search_locale_with_names(self): - p = parser.DateTimeParser('sv_se') + p = parser.DateTimeParser("sv_se") - assertEqual( - p.parse('Jan föddes den 31 Dec 1980', 'DD MMM YYYY'), - datetime(1980, 12, 31)) + self.assertEqual( + p.parse("Jan föddes den 31 Dec 1980", "DD MMM YYYY"), datetime(1980, 12, 31) + ) - assertEqual( - p.parse('Jag föddes den 25 Augusti 1975', 'DD MMMM YYYY'), - datetime(1975, 8, 25)) + self.assertEqual( + p.parse("Jag föddes den 25 Augusti 1975", "DD MMMM YYYY"), + datetime(1975, 8, 25), + ) def test_parse_search_fails(self): - with assertRaises(parser.ParserError): - self.parser.parse('Jag föddes den 25 Augusti 1975', 'DD MMMM YYYY') + with self.assertRaises(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" - assertEqual( + self.assertEqual( self.parser.parse("Thursday, December 10, 2015 at 5:09pm", format), - datetime(2015, 12, 10, 17, 9)) + datetime(2015, 12, 10, 17, 9), + ) format = "[MMMM] M D, YYYY [at] h:mma" - assertEqual( + self.assertEqual( self.parser.parse("MMMM 12 10, 2015 at 5:09pm", format), - datetime(2015, 12, 10, 17, 9)) + datetime(2015, 12, 10, 17, 9), + ) format = "[It happened on] MMMM Do [in the year] YYYY [a long time ago]" - assertEqual( - self.parser.parse("It happened on November 25th in the year 1990 a long time ago", format), - datetime(1990, 11, 25)) + self.assertEqual( + self.parser.parse( + "It happened on November 25th in the year 1990 a long time ago", format + ), + datetime(1990, 11, 25), + ) format = "[It happened on] MMMM Do [in the][ year] YYYY [a long time ago]" - assertEqual( - self.parser.parse("It happened on November 25th in the year 1990 a long time ago", format), - datetime(1990, 11, 25)) + self.assertEqual( + self.parser.parse( + "It happened on November 25th in the year 1990 a long time ago", format + ), + datetime(1990, 11, 25), + ) format = "[I'm][ entirely][ escaped,][ weee!]" - assertEqual( - self.parser.parse("I'm entirely escaped, weee!", format), - datetime(1, 1, 1)) + self.assertEqual( + self.parser.parse("I'm entirely escaped, weee!", format), datetime(1, 1, 1) + ) # Special RegEx characters - format = 'MMM DD, YYYY |^${}().*+?<>-& h:mm A' - assertEqual( + format = "MMM DD, YYYY |^${}().*+?<>-& h:mm A" + self.assertEqual( self.parser.parse("Dec 31, 2017 |^${}().*+?<>-& 2:00 AM", format), - datetime(2017, 12, 31, 2, 0)) + datetime(2017, 12, 31, 2, 0), + ) From 9ca3296e028f88e787a84b3d7893453776e7cbb8 Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Fri, 31 May 2019 09:49:48 +0100 Subject: [PATCH 183/649] Docs update for 0.14.0 (#595) * Docs update for 0.14.0 * Bump docs to 0.14.0 --- docs/conf.py | 2 +- docs/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 827d143a6..527f99bee 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,7 +23,7 @@ author = "Chris Smith" # The full version, including alpha/beta/rc tags -release = "0.13.2" +release = "0.14.0" # -- General configuration --------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index 87d422599..fd5376f1c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,7 +26,7 @@ Features -------- - Fully implemented, drop-in replacement for datetime -- Supports Python 2.7, 3.4, 3.5, 3.6 and 3.7 +- Supports Python 2.7, 3.5, 3.6, 3.7 and 3.8 - Timezone-aware & UTC by default - Provides super-simple creation options for many common input scenarios - Updated .replace method with support for relative offsets, including weeks From bad059f2330469220de86c73e439e2b2eb0d8084 Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Fri, 31 May 2019 10:18:38 +0100 Subject: [PATCH 184/649] Update HISTORY.MD with 0.14.0 (#596) --- HISTORY.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index df2b95d83..e6653121c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,14 @@ ## History +### 0.14.0 + +- [NEW] Added provisional support for Python 3.8. +- [CHANGE] Removed support for EOL Python 3.4. +- [FIX] Updated setup.py with modern Python standards. +- [FIX] Upgraded dependencies to latest versions. +- [FIX] Enabled flake8 and black on travis builds. +- [FIX] Formatted code using black and isort. + ### 0.13.2 - [NEW] Add is_between method. From cfafc0e4b2f2fcf2b004cb0ab2a67e2bb5feb56d Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 31 May 2019 18:29:31 -0700 Subject: [PATCH 185/649] version bump to 0.14.0 --- arrow/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/__init__.py b/arrow/__init__.py index 9d2b85293..1882ba02c 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -4,4 +4,4 @@ from .arrow import Arrow from .factory import ArrowFactory -__version__ = "0.13.2" +__version__ = "0.14.0" From f77546d8821304e0442be4c275d80e01c61c3a81 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 3 Jun 2019 05:17:50 -0400 Subject: [PATCH 186/649] Fixed regression from 0.14.0 (#599) * Fixed regression from 0.14.0 * Fix unpack * Updated versions and history and fixed grep function --- HISTORY.md | 4 ++++ arrow/__init__.py | 2 +- setup.py | 16 +++++++++++++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index e6653121c..dd1c31ae3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,9 @@ ## History +### 0.14.1 + +- [FIX] Fixed "ImportError: No module named 'dateutil'" (#598). + ### 0.14.0 - [NEW] Added provisional support for Python 3.8. diff --git a/arrow/__init__.py b/arrow/__init__.py index 1882ba02c..39546b1c4 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -4,4 +4,4 @@ from .arrow import Arrow from .factory import ArrowFactory -__version__ = "0.14.0" +__version__ = "0.14.1" diff --git a/setup.py b/setup.py index 4a2febcb0..ad611c99f 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,24 @@ # -*- coding: utf-8 -*- -from setuptools import setup +import re -from arrow import __version__ +from setuptools import setup with open("README.rst", "r") as f: readme = f.read() +with open("arrow/__init__.py", "r") as f: + init = f.read() + + +def get_version(): + pattern = r'{}\W*=\W*"([^"]+)"'.format("__version__") + strval = re.findall(pattern, init)[0] + return strval + + setup( name="arrow", - version=__version__, + version=get_version(), description="Better dates and times for Python", long_description=readme, long_description_content_type="text/x-rst", From 95f2be6f556984bf4cacc85bc053db451595ec3e Mon Sep 17 00:00:00 2001 From: Andrew Wilson Date: Tue, 4 Jun 2019 12:53:32 +1000 Subject: [PATCH 187/649] Always open README and __init__.pyfiles with utf-8 encoding (#601) * Always open README with utf-8 encoding * Always open init.py with utf-8 encoding --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index ad611c99f..e46587651 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- +import codecs import re from setuptools import setup -with open("README.rst", "r") as f: +with codecs.open("README.rst", encoding="utf-8") as f: readme = f.read() -with open("arrow/__init__.py", "r") as f: +with codecs.open("arrow/__init__.py", encoding="utf-8") as f: init = f.read() From 26a8af8d47eb2cec32033283769f67310cd5b85b Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 4 Jun 2019 05:29:10 -0400 Subject: [PATCH 188/649] Updated tox file and changed Travis CI to use tox (#597) * Updated tox file and changed Travis CI to use tox * Tweak travis yml * Added environment to 3.8 * Fix CI builds with changes from emergency fix pr * Fix unpack * Updated setup.py * Removed intermediate variable from get_version() * Added test-dev target to Makefile and added tests back to coverage * Updated HISTORY and bump version to 0.14.2 * Replaced codecs with io in setup.py after some research --- .pre-commit-config.yaml | 2 +- .travis.yml | 18 +++++++++--------- HISTORY.md | 5 +++++ Makefile | 40 ++++++++++++++-------------------------- arrow/__init__.py | 3 +-- setup.cfg | 7 +++++-- setup.py | 9 ++++----- tox.ini | 27 +++++++++++---------------- 8 files changed, 50 insertions(+), 61 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f82c849ab..5e4e41268 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v1.17.1 + rev: v1.18.0 hooks: - id: pyupgrade - repo: https://github.com/pre-commit/pygrep-hooks diff --git a/.travis.yml b/.travis.yml index 16b0f81b6..d45f032d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,24 +3,24 @@ dist: xenial matrix: include: - python: 2.7 + env: TOXENV=py27 - python: 3.5 + env: TOXENV=py35 - python: 3.6 + env: TOXENV=py36 - python: 3.7 + env: TOXENV=py37 - python: 3.8-dev + env: TOXENV=py38 - name: "Linting" python: 3.7 - env: TARGET=lint-ci + env: TOXENV=lint cache: directories: - $HOME/.cache/pre-commit allow_failures: - python: 3.8-dev -before_install: pip install -U codecov -install: - - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then make build27; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then make build35; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then make build36; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.7' ]]; then make build37; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.8-dev' ]]; then make build38; fi -script: make "${TARGET:-test}" + env: TOXENV=py38 +install: pip install -U codecov tox +script: tox after_success: codecov diff --git a/HISTORY.md b/HISTORY.md index dd1c31ae3..221df67a6 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ ## History +### 0.14.2 + +- [CHANGE] Travis CI builds now use tox to lint and run tests. +- [FIX] Fixed UnicodeDecodeError on certain locales (#600). + ### 0.14.1 - [FIX] Fixed "ImportError: No module named 'dateutil'" (#598). diff --git a/Makefile b/Makefile index de2167ff0..bb3cdd1b5 100644 --- a/Makefile +++ b/Makefile @@ -2,28 +2,14 @@ auto: build27 -build27: - virtualenv local --python=python2.7 - local/bin/pip install -r requirements.txt - local/bin/pre-commit install - -build35: - virtualenv local --python=python3.5 - local/bin/pip install -r requirements.txt - local/bin/pre-commit install - -build36: - virtualenv local --python=python3.6 - local/bin/pip install -r requirements.txt - local/bin/pre-commit install - -build37: - virtualenv local --python=python3.7 - local/bin/pip install -r requirements.txt - local/bin/pre-commit install - -build38: - virtualenv local --python=python3.8 +build27: PYTHON_VER = python2.7 +build35: PYTHON_VER = python3.5 +build36: PYTHON_VER = python3.6 +build37: PYTHON_VER = python3.7 +build38: PYTHON_VER = python3.8 + +build27 build35 build36 build37 build38: + virtualenv local --python=$(PYTHON_VER) local/bin/pip install -r requirements.txt local/bin/pre-commit install @@ -31,10 +17,11 @@ test: rm -f .coverage . local/bin/activate && nosetests -lint: - local/bin/pre-commit run --all-files +test-dev: + rm -f .coverage + . local/bin/activate && python -Wd -m nose -lint-ci: +lint: local/bin/pre-commit run --all-files --show-diff-on-failure docs: @@ -42,5 +29,6 @@ docs: . local/bin/activate && cd docs; make html clean: - rm -rf local ./**/__pycache__ + rm -rf local .tox ./**/__pycache__ + rm -rf dist build .egg arrow.egg-info rm -f ./**/*.pyc .coverage diff --git a/arrow/__init__.py b/arrow/__init__.py index 39546b1c4..7fad091bd 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- - from .api import get, now, utcnow from .arrow import Arrow from .factory import ArrowFactory -__version__ = "0.14.1" +__version__ = "0.14.2" diff --git a/setup.cfg b/setup.cfg index e08567216..fa26fd973 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,13 +8,13 @@ cover-package = arrow cover-erase = true [coverage:run] -branch = True +branch = true source = arrow tests [coverage:report] -show_missing = True +show_missing = true fail_under = 100 [flake8] @@ -22,6 +22,9 @@ per-file-ignores = arrow/__init__.py:F401 ignore = E203,E501,W503 [tool:isort] +line_length = 88 +multi_line_output = 3 +include_trailing_comma = true known_third_party = chai,dateutil,pytz,setuptools,simplejson [bdist_wheel] diff --git a/setup.py b/setup.py index e46587651..7f467c5a6 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,19 @@ # -*- coding: utf-8 -*- -import codecs +import io import re from setuptools import setup -with codecs.open("README.rst", encoding="utf-8") as f: +with io.open("README.rst", "r", encoding="utf-8") as f: readme = f.read() -with codecs.open("arrow/__init__.py", encoding="utf-8") as f: +with io.open("arrow/__init__.py", "r", encoding="utf-8") as f: init = f.read() def get_version(): pattern = r'{}\W*=\W*"([^"]+)"'.format("__version__") - strval = re.findall(pattern, init)[0] - return strval + return re.findall(pattern, init)[0] setup( diff --git a/tox.ini b/tox.ini index fb74896fa..b6380ab77 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,15 @@ [tox] -envlist = py27,py35,py36,py37 -skip_missing_interpreters = True - -[common] -deps = - nose - nose-cov - simplejson +envlist = py{27,35,36,37,38},lint +skip_missing_interpreters = true [testenv] -deps = - {[common]deps} - chai -commands = nosetests --all-modules --with-coverage arrow tests +deps = -rrequirements.txt +commands = + nosetests + pre-commit install -[testenv:py26] -deps = - {[common]deps} - chai==0.3.1 +[testenv:lint] +basepython = python3 +skip_install = true +deps = pre-commit +commands = pre-commit run --all-files --show-diff-on-failure From 47445f99a5c9935c899bc803947f9628b62c2aed Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 22 Jun 2019 21:16:31 -0400 Subject: [PATCH 189/649] started some fixes for get method --- arrow/parser.py | 51 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 7266ff957..e2e73ac11 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -91,39 +91,60 @@ def parse_iso(self, string): has_time = "T" in string or " " in string.strip() space_divider = " " in string.strip() + has_tz = False + if has_time: if space_divider: date_string, time_string = string.split(" ", 1) else: date_string, time_string = string.split("T", 1) + time_parts = re.split("[+-]", time_string, 1) + colon_count = time_parts[0].count(":") + has_tz = len(time_parts) > 1 - has_seconds = time_parts[0].count(":") > 1 + has_hours = colon_count == 0 + has_minutes = colon_count == 1 + has_seconds = colon_count == 2 has_subseconds = re.search("[.,]", time_parts[0]) if has_subseconds: - formats = ["YYYY-MM-DDTHH:mm:ss%sS" % has_subseconds.group()] + time_string = "HH:mm:ss{}S".format(has_subseconds.group()) elif has_seconds: - formats = ["YYYY-MM-DDTHH:mm:ss"] + time_string = "HH:mm:ss" + elif has_minutes: + time_string = "HH:mm" + elif has_hours: + time_string = "HH" else: - formats = ["YYYY-MM-DDTHH:mm"] - else: - has_tz = False - # generate required formats: YYYY-MM-DD, YYYY-MM-DD, YYYY - # using various separators: -, /, . - len_markers = len(self.MARKERS) - formats = [ - separator.join(self.MARKERS[: len_markers - i]) - for i in range(len_markers) - for separator in self.SEPARATORS - ] + # TODO: improve error message + # ! TODO: add tests for new conditional cases + raise ValueError("ISO 8601 time string expected.") + + # required ISO 8601 formats + formats = [ + "YYYY-MM-DD", + "YYYY/MM/DD", + "YYYY.MM.DD", + "YYYY-MM", + "YYYY/MM", + "YYYY.MM", + "YYYY", + ] + + # !? NOTE: ASK CHRIS ABOUT . SEPARATOR => I am not sure if it is part of ISO 8601? + + if has_time: + formats = ["{}T{}".format(f, time_string) for f in formats] if has_time and has_tz: - formats = [f + "Z" for f in formats] + formats = ["{}Z".format(f) for f in formats] if space_divider: formats = [item.replace("T", " ", 1) for item in formats] + # ! IDEA: pass in a flag to denote that we are coming from a get() + # request with no formatting string was passed in return self._parse_multiformat(string, formats) def _generate_pattern_re(self, fmt): From df49462a04706d9cbce0b974fb6d8a5a5ff45783 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 23 Jun 2019 12:20:27 -0400 Subject: [PATCH 190/649] Current progress of fixing get --- .gitignore | 1 + arrow/factory.py | 2 +- arrow/parser.py | 32 ++++++++++++++------ arrow/util.py | 69 +++--------------------------------------- tests/arrow_tests.py | 64 --------------------------------------- tests/factory_tests.py | 6 ++-- tests/parser_tests.py | 14 ++++++--- 7 files changed, 41 insertions(+), 147 deletions(-) diff --git a/.gitignore b/.gitignore index ae9b3e31a..9eded2397 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ docs/_build/ # VS Code .vscode/ +.idea/ diff --git a/arrow/factory.py b/arrow/factory.py index 8591443a4..d6bf4dffa 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -150,7 +150,7 @@ def get(self, *args, **kwargs): if arg is None: return self.type.utcnow() - # try (int, float, str(int), str(float)) -> utc, from timestamp. + # try (int, float) -> utc, from timestamp. if is_timestamp(arg): return self.type.utcfromtimestamp(arg) diff --git a/arrow/parser.py b/arrow/parser.py index e2e73ac11..d22a23515 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -99,15 +99,19 @@ def parse_iso(self, string): else: date_string, time_string = string.split("T", 1) + # ! TODO: look for Z in time string? time_parts = re.split("[+-]", time_string, 1) colon_count = time_parts[0].count(":") + # is_basic_format = colon_count > 0 + has_tz = len(time_parts) > 1 has_hours = colon_count == 0 has_minutes = colon_count == 1 has_seconds = colon_count == 2 has_subseconds = re.search("[.,]", time_parts[0]) + # TODO: Add support for basic timestamps if has_subseconds: time_string = "HH:mm:ss{}S".format(has_subseconds.group()) elif has_seconds: @@ -117,26 +121,25 @@ def parse_iso(self, string): elif has_hours: time_string = "HH" else: - # TODO: improve error message # ! TODO: add tests for new conditional cases - raise ValueError("ISO 8601 time string expected.") + raise ValueError("No valid time component provided.") - # required ISO 8601 formats + # required date formats to test against formats = [ "YYYY-MM-DD", "YYYY/MM/DD", "YYYY.MM.DD", + "YYYYMMDD", "YYYY-MM", "YYYY/MM", "YYYY.MM", "YYYY", ] - # !? NOTE: ASK CHRIS ABOUT . SEPARATOR => I am not sure if it is part of ISO 8601? - if has_time: formats = ["{}T{}".format(f, time_string) for f in formats] + # TODO: what if someone adds a Z already? if has_time and has_tz: formats = ["{}Z".format(f) for f in formats] @@ -145,7 +148,7 @@ def parse_iso(self, string): # ! IDEA: pass in a flag to denote that we are coming from a get() # request with no formatting string was passed in - return self._parse_multiformat(string, formats) + return self._parse_multiformat(string, formats, True) def _generate_pattern_re(self, fmt): @@ -198,7 +201,7 @@ def _generate_pattern_re(self, fmt): return tokens, re.compile(final_fmt_pattern, flags=re.IGNORECASE) - def parse(self, string, fmt): + def parse(self, string, fmt, from_parse_iso=False): if isinstance(fmt, list): return self._parse_multiformat(string, fmt) @@ -212,6 +215,17 @@ def parse(self, string, fmt): fmt_pattern_re.pattern, string ) ) + + if from_parse_iso: + if match.start() != 0: + raise ParserError + + if string[-1] == "Z" and match.end() != len(string): + raise ParserError + + if string[-1] != "Z" and match.end() != len(string): + raise ParserError + parts = {} for token in fmt_tokens: if token == "Do": @@ -307,13 +321,13 @@ def _build_datetime(parts): tzinfo=parts.get("tzinfo"), ) - def _parse_multiformat(self, string, formats): + def _parse_multiformat(self, string, formats, from_parse_iso=False): _datetime = None for fmt in formats: try: - _datetime = self.parse(string, fmt) + _datetime = self.parse(string, fmt, from_parse_iso) break except ParserError: pass diff --git a/arrow/util.py b/arrow/util.py index 03132f7ed..8a379a4ef 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -import sys -import warnings +from datetime import datetime def total_seconds(td): # pragma: no cover @@ -10,12 +9,12 @@ def total_seconds(td): # pragma: no cover def is_timestamp(value): - if type(value) == bool: + if isinstance(value, bool): return False try: - float(value) + datetime.fromtimestamp(value) return True - except Exception: + except TypeError: return False @@ -34,62 +33,4 @@ def isstr(s): return isinstance(s, str) -class list_to_iter_shim(list): - """ A temporary shim for functions that currently return a list but that will, after a - deprecation period, return an iteratator. - """ - - def __init__(self, iterable=(), **kwargs): - """ Equivalent to list(iterable). warn_text will be emitted on all non-iterator operations. - """ - self._warn_text = ( - kwargs.pop("warn_text", None) - or "this object will be converted to an iterator in a future release" - ) - self._iter_count = 0 - list.__init__(self, iterable, **kwargs) - - def _warn(self): - warnings.warn(self._warn_text, DeprecationWarning) - - def __iter__(self): - self._iter_count += 1 - if self._iter_count > 1: - self._warn() - return list.__iter__(self) - - def _wrap_method(name): - list_func = getattr(list, name) - - def wrapper(self, *args, **kwargs): - self._warn() - return list_func(self, *args, **kwargs) - - return wrapper - - __contains__ = _wrap_method("__contains__") - __add__ = _wrap_method("__add__") - __mul__ = _wrap_method("__mul__") - __getitem__ = _wrap_method("__getitem__") - # Ideally, we would throw warnings from __len__, but list(x) calls len(x) - index = _wrap_method("index") - count = _wrap_method("count") - __setitem__ = _wrap_method("__setitem__") - __delitem__ = _wrap_method("__delitem__") - append = _wrap_method("append") - if sys.version_info.major >= 3: # pragma: no cover - clear = _wrap_method("clear") - copy = _wrap_method("copy") - extend = _wrap_method("extend") - __iadd__ = _wrap_method("__iadd__") - __imul__ = _wrap_method("__imul__") - insert = _wrap_method("insert") - pop = _wrap_method("pop") - remove = _wrap_method("remove") - reverse = _wrap_method("reverse") - sort = _wrap_method("sort") - - del _wrap_method - - -__all__ = ["total_seconds", "is_timestamp", "isstr", "list_to_iter_shim"] +__all__ = ["total_seconds", "is_timestamp", "isstr"] diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 825db674c..c88959b0d 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1814,67 +1814,3 @@ def test_get_iteration_params(self): with self.assertRaises(Exception): arrow.Arrow._get_iteration_params(None, None) - - def test_list_to_iter_shim(self): - def newshim(): - return util.list_to_iter_shim(range(5), warn_text="testing") - - # Iterating over a shim once should not throw a warning - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - - iter(newshim()) - list(newshim()) - for _ in newshim(): - pass - len(newshim()) # ...because it's called by `list(x)` - - self.assertEqual([], w) - - # Iterating over a shim twice (or more) should throw a warning - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - shim = newshim() - - for _ in shim: - pass - for _ in shim: - pass - - self.assertEqual(1, len(w)) - self.assertEqual(w[0].category, DeprecationWarning) - self.assertEqual("testing", w[0].message.args[0]) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - shim = newshim() - - 0 in shim - shim + [] - shim * 1 - shim[0] - shim.index(0) - shim.count(0) - - shim[0:0] = [] # doesn't warn on py2 - del shim[0:0] # doesn't warn on py2 - newshim().append(6) - if sys.version_info.major >= 3: # pragma: no cover - newshim().clear() - shim.copy() - shim.extend([]) - shim += [] - shim *= 1 - newshim().insert(0, 6) - shim.pop(-1) - newshim().remove(0) - newshim().reverse() - newshim().sort() - - if sys.version_info.major >= 3: # pragma: no cover - self.assertEqual(19, len(w)) - else: # pragma: no cover - self.assertEqual(15, len(w)) - for warn in w: - self.assertEqual(warn.category, DeprecationWarning) - self.assertEqual("testing", warn.message.args[0]) diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 21e5ae4f6..ef0b60d01 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -25,7 +25,7 @@ def test_no_args(self): def test_timestamp_one_arg_no_arg(self): - no_arg = self.factory.get("1406430900").timestamp + no_arg = self.factory.get(1406430900).timestamp one_arg = self.factory.get("1406430900", "X").timestamp self.assertEqual(no_arg, one_arg) @@ -49,16 +49,14 @@ def test_one_arg_timestamp(self): timestamp_dt = datetime.utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) self.assertEqual(self.factory.get(timestamp), timestamp_dt) - self.assertEqual(self.factory.get(str(timestamp)), timestamp_dt) timestamp = 123.45 timestamp_dt = datetime.utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) self.assertEqual(self.factory.get(timestamp), timestamp_dt) - self.assertEqual(self.factory.get(str(timestamp)), timestamp_dt) # Issue 216 - timestamp = "99999999999999999999999999" + timestamp = 99999999999999999999999999 # Python 3 raises `OverflowError`, Python 2 raises `ValueError` with self.assertRaises((OverflowError, ValueError)): self.factory.get(timestamp) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index a212f0426..f9d0e3a42 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -22,8 +22,10 @@ def test_parse_multiformat(self): mock_datetime = self.mock() - self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserError) - self.expect(self.parser.parse).args("str", "fmt_b").returns(mock_datetime) + self.expect(self.parser.parse).args("str", "fmt_a", False).raises(ParserError) + self.expect(self.parser.parse).args("str", "fmt_b", False).returns( + mock_datetime + ) result = self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) @@ -31,8 +33,8 @@ def test_parse_multiformat(self): def test_parse_multiformat_all_fail(self): - self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserError) - self.expect(self.parser.parse).args("str", "fmt_b").raises(ParserError) + self.expect(self.parser.parse).args("str", "fmt_a", False).raises(ParserError) + self.expect(self.parser.parse).args("str", "fmt_b", False).raises(ParserError) with self.assertRaises(ParserError): self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) @@ -41,7 +43,9 @@ def test_parse_multiformat_unself_expected_fail(self): class UnselfExpectedError(Exception): pass - self.expect(self.parser.parse).args("str", "fmt_a").raises(UnselfExpectedError) + self.expect(self.parser.parse).args("str", "fmt_a", False).raises( + UnselfExpectedError + ) with self.assertRaises(UnselfExpectedError): self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) From 9de4b84efbdcf34454e53d0d1761d20fea477dee Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 23 Jun 2019 14:51:23 -0400 Subject: [PATCH 191/649] Updated name of virtualenv from local to venv and updated gitignore (#602) * Updated virtual env to venv rather than local in Makefile and added to gitignore * Fully remove .vscode and .idea * Added test.py to ignore --- .gitignore | 216 ++++++++++++++++++++++++++++++++++++++++++++++++----- Makefile | 16 ++-- 2 files changed, 207 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index ae9b3e31a..cee28fdd6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,209 @@ -*.pyc -*.egg-info +# Small entry point file for debugging tasks +test.py -# temporary files created by VIM -*.swp -*.swo +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class -# temporary files created by git/patch -*.orig +# C extensions +*.so +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ .coverage +.coverage.* +.cache nosetests.xml -tests/.noseids +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ -.venv/ -local/ -build/ -dist/ +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation docs/_build/ -.idea -.DS_Store -# tox -.tox +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json -# VS Code +# Pyre type checker +.pyre/ + +# Swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +.idea/ .vscode/ + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk diff --git a/Makefile b/Makefile index bb3cdd1b5..ec29f2aea 100644 --- a/Makefile +++ b/Makefile @@ -9,26 +9,26 @@ build37: PYTHON_VER = python3.7 build38: PYTHON_VER = python3.8 build27 build35 build36 build37 build38: - virtualenv local --python=$(PYTHON_VER) - local/bin/pip install -r requirements.txt - local/bin/pre-commit install + virtualenv venv --python=$(PYTHON_VER) + venv/bin/pip install -r requirements.txt + venv/bin/pre-commit install test: rm -f .coverage - . local/bin/activate && nosetests + . venv/bin/activate && nosetests test-dev: rm -f .coverage - . local/bin/activate && python -Wd -m nose + . venv/bin/activate && python -Wd -m nose lint: - local/bin/pre-commit run --all-files --show-diff-on-failure + venv/bin/pre-commit run --all-files --show-diff-on-failure docs: touch docs/index.rst - . local/bin/activate && cd docs; make html + . venv/bin/activate && cd docs; make html clean: - rm -rf local .tox ./**/__pycache__ + rm -rf venv .tox ./**/__pycache__ rm -rf dist build .egg arrow.egg-info rm -f ./**/*.pyc .coverage From d69d16c89f49adb54302b16fabea4539445f040a Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Mon, 24 Jun 2019 15:50:43 +0100 Subject: [PATCH 192/649] Create custom warning class for get changes --- arrow/parser.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/arrow/parser.py b/arrow/parser.py index d22a23515..3883cee55 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals import re +import warnings from datetime import datetime from dateutil import tz @@ -18,6 +19,18 @@ class ParserError(RuntimeError): pass +class GetParseWarning(DeprecationWarning): + """Raised when .get() is passed a string with no formats and matches incorrectly + on one of the default formats. + + e.g. + arrow.get('blabla2016') -> + arrow.get('13/4/2045') -> + + In version 0.15.0 this will become a ParserError. + """ + + class DateTimeParser(object): _FORMAT_RE = re.compile( @@ -218,12 +231,34 @@ def parse(self, string, fmt, from_parse_iso=False): if from_parse_iso: if match.start() != 0: + warnings.warn( + "Parser loosely matched {fmt} on '{string}', in the " + "future this will raise a ParserError.".format( + fmt=fmt, string=string + ), + category=GetParseWarning, + ) raise ParserError if string[-1] == "Z" and match.end() != len(string): + # TODO what about 2019-06-24T10:45:31Z + warnings.warn( + "Parser loosely matched {fmt} on '{string}', in the " + "future this will raise a ParserError.".format( + fmt=fmt, string=string + ), + category=GetParseWarning, + ) raise ParserError if string[-1] != "Z" and match.end() != len(string): + warnings.warn( + "Parser loosely matched {fmt} on '{string}', in the " + "future this will raise a ParserError.".format( + fmt=fmt, string=string + ), + category=GetParseWarning, + ) raise ParserError parts = {} From a50137f1ac37d486ef90ac188827965dc2ace04c Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 29 Jun 2019 11:15:47 -0400 Subject: [PATCH 193/649] More progress toward GET fixes --- arrow/parser.py | 8 ++++++-- tests/parser_tests.py | 8 ++++---- tests/util_tests.py | 23 +++++++++++++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 tests/util_tests.py diff --git a/arrow/parser.py b/arrow/parser.py index d22a23515..aec8661b0 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -105,7 +105,7 @@ def parse_iso(self, string): # is_basic_format = colon_count > 0 - has_tz = len(time_parts) > 1 + has_tz = len(time_parts) > 1 or string[-1] == "Z" has_hours = colon_count == 0 has_minutes = colon_count == 1 has_seconds = colon_count == 2 @@ -217,12 +217,15 @@ def parse(self, string, fmt, from_parse_iso=False): ) if from_parse_iso: + # Accounts for cases such as "blahblah2016" if match.start() != 0: raise ParserError - if string[-1] == "Z" and match.end() != len(string): + # Accounts for cases such as "2016-05T04:05:06.78912blahZ" + if string[-1] == "Z" and match.end() != len(string) - 1: raise ParserError + # Accounts for cases such as "2016-05T04:05:06.78912Zblah" if string[-1] != "Z" and match.end() != len(string): raise ParserError @@ -239,6 +242,7 @@ def _parse_token(self, token, value, parts): if token == "YYYY": parts["year"] = int(value) + elif token == "YY": value = int(value) parts["year"] = 1900 + value if value > 68 else 2000 + value diff --git a/tests/parser_tests.py b/tests/parser_tests.py index f9d0e3a42..88fb8f484 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -562,11 +562,11 @@ def test_YYYY_MM_DDTHH_mm_ss_SZ(self): datetime(2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzoffset(None, 3600)), ) + parsed = self.parser.parse_iso("2013-02-03 04:05:06.78912Z") + + expected = datetime(2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzoffset(None, 0)) # Properly parse string with Z timezone - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.78912Z"), - datetime(2013, 2, 3, 4, 5, 6, 789120), - ) + self.assertEqual(parsed, expected) def test_gnu_date(self): """ diff --git a/tests/util_tests.py b/tests/util_tests.py new file mode 100644 index 000000000..693ba9762 --- /dev/null +++ b/tests/util_tests.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from datetime import datetime + +from chai import Chai + +from arrow import util + + +class UtilTests(Chai): + def test_is_timestamp(self): + timestamp_float = datetime.now().timestamp() + timestamp_int = int(timestamp_float) + + self.assertTrue(util.is_timestamp(timestamp_int)) + self.assertTrue(util.is_timestamp(timestamp_float)) + + self.assertFalse(util.is_timestamp(str(timestamp_int))) + self.assertFalse(util.is_timestamp(str(timestamp_float))) + self.assertFalse(util.is_timestamp(True)) + self.assertFalse(util.is_timestamp(False)) + + full_datetime = "2019-06-23T13:12:42" + self.assertFalse(util.is_timestamp(full_datetime)) From 12c1b4c67b2a5027ec005ddd5b56963cce24c480 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 29 Jun 2019 12:04:43 -0400 Subject: [PATCH 194/649] Reverted change to handling of Z string --- arrow/parser.py | 9 +++------ tests/parser_tests.py | 11 ++++++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 8c5947f83..8947ea9d7 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -112,13 +112,13 @@ def parse_iso(self, string): else: date_string, time_string = string.split("T", 1) - # ! TODO: look for Z in time string? + # TODO: understand why we are not accounting for Z directly time_parts = re.split("[+-]", time_string, 1) colon_count = time_parts[0].count(":") # is_basic_format = colon_count > 0 - has_tz = len(time_parts) > 1 or string[-1] == "Z" + has_tz = len(time_parts) > 1 has_hours = colon_count == 0 has_minutes = colon_count == 1 has_seconds = colon_count == 2 @@ -134,7 +134,7 @@ def parse_iso(self, string): elif has_hours: time_string = "HH" else: - # ! TODO: add tests for new conditional cases + # TODO: add tests for new conditional cases raise ValueError("No valid time component provided.") # required date formats to test against @@ -152,15 +152,12 @@ def parse_iso(self, string): if has_time: formats = ["{}T{}".format(f, time_string) for f in formats] - # TODO: what if someone adds a Z already? if has_time and has_tz: formats = ["{}Z".format(f) for f in formats] if space_divider: formats = [item.replace("T", " ", 1) for item in formats] - # ! IDEA: pass in a flag to denote that we are coming from a get() - # request with no formatting string was passed in return self._parse_multiformat(string, formats, True) def _generate_pattern_re(self, fmt): diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 88fb8f484..d4c3872fb 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -562,11 +562,12 @@ def test_YYYY_MM_DDTHH_mm_ss_SZ(self): datetime(2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzoffset(None, 3600)), ) - parsed = self.parser.parse_iso("2013-02-03 04:05:06.78912Z") - - expected = datetime(2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzoffset(None, 0)) - # Properly parse string with Z timezone - self.assertEqual(parsed, expected) + # TODO: Shouldn't the datetime object being compared to have a tz offset of 0? + # Test fails if this offset is added. + self.assertEqual( + self.parser.parse_iso("2013-02-03 04:05:06.78912Z"), + datetime(2013, 2, 3, 4, 5, 6, 789120), + ) def test_gnu_date(self): """ From b67c9e2e83a386acf57f2c5a9347cef0c6baf8af Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 29 Jun 2019 13:15:38 -0400 Subject: [PATCH 195/649] Added a number of new formatting changes --- arrow/parser.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 8947ea9d7..53a220e43 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -116,17 +116,16 @@ def parse_iso(self, string): time_parts = re.split("[+-]", time_string, 1) colon_count = time_parts[0].count(":") - # is_basic_format = colon_count > 0 + is_basic_time_format = colon_count == 0 has_tz = len(time_parts) > 1 - has_hours = colon_count == 0 - has_minutes = colon_count == 1 - has_seconds = colon_count == 2 - has_subseconds = re.search("[.,]", time_parts[0]) - - # TODO: Add support for basic timestamps - if has_subseconds: - time_string = "HH:mm:ss{}S".format(has_subseconds.group()) + has_hours = colon_count == 0 or len(time_string) == 2 + has_minutes = colon_count == 1 or len(time_string) == 4 + has_seconds = colon_count == 2 or len(time_string) == 6 + has_sub_seconds = re.search("[.,]", time_parts[0]) + + if has_sub_seconds: + time_string = "HH:mm:ss{}S".format(has_sub_seconds.group()) elif has_seconds: time_string = "HH:mm:ss" elif has_minutes: @@ -137,16 +136,26 @@ def parse_iso(self, string): # TODO: add tests for new conditional cases raise ValueError("No valid time component provided.") + if is_basic_time_format: + time_string = time_string.replace(":", "") + # required date formats to test against formats = [ "YYYY-MM-DD", + "YYYY-M-DD", + "YYYY-M-D", "YYYY/MM/DD", + "YYYY/M/DD", + "YYYY/M/D", "YYYY.MM.DD", + "YYYY.M.DD", + "YYYY.M.D", "YYYYMMDD", "YYYY-MM", "YYYY/MM", "YYYY.MM", "YYYY", + "YY", ] if has_time: From 824b7445c8f615700b2f14b86274faef791f7d22 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 30 Jun 2019 14:51:20 -0400 Subject: [PATCH 196/649] Removed unnecessary helper functions in parser class and cleaned up some variable names --- .pre-commit-config.yaml | 4 ++-- arrow/parser.py | 33 ++++++++++----------------------- tests/parser_tests.py | 11 ----------- 3 files changed, 12 insertions(+), 36 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e4e41268..9f950d481 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,11 +15,11 @@ repos: hooks: - id: seed-isort-config - repo: https://github.com/pre-commit/mirrors-isort - rev: v4.3.20 + rev: v4.3.21 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v1.18.0 + rev: v1.19.0 hooks: - id: pyupgrade - repo: https://github.com/pre-commit/pygrep-hooks diff --git a/arrow/parser.py b/arrow/parser.py index 53a220e43..ea677d8f2 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -139,6 +139,7 @@ def parse_iso(self, string): if is_basic_time_format: time_string = time_string.replace(":", "") + # TODO: add tests for all the new formats # required date formats to test against formats = [ "YYYY-MM-DD", @@ -209,14 +210,13 @@ def _generate_pattern_re(self, fmt): offset += len(input_pattern) - (m.end() - m.start()) final_fmt_pattern = "" - a = fmt_pattern.split(r"\#") - b = escaped_data + split_fmt = fmt_pattern.split(r"\#") # Due to the way Python splits, 'a' will always be longer - for i in range(len(a)): - final_fmt_pattern += a[i] - if i < len(b): - final_fmt_pattern += b[i][1:-1] + for i in range(len(split_fmt)): + final_fmt_pattern += split_fmt[i] + if i < len(escaped_data): + final_fmt_pattern += escaped_data[i][1:-1] return tokens, re.compile(final_fmt_pattern, flags=re.IGNORECASE) @@ -277,6 +277,7 @@ def parse(self, string, fmt, from_parse_iso=False): else: value = match.group(token) self._parse_token(token, value, parts) + return self._build_datetime(parts) def _parse_token(self, token, value, parts): @@ -379,27 +380,13 @@ def _parse_multiformat(self, string, formats, from_parse_iso=False): if _datetime is None: raise ParserError( - "Could not match input to any of {} on '{}'".format(formats, string) + "Could not match input '{}' to any of the supported formats: {}".format( + string, ", ".join(formats) + ) ) return _datetime - @staticmethod - def _map_lookup(input_map, key): - - try: - return input_map[key] - except KeyError: - raise ParserError('Could not match "{}" to {}'.format(key, input_map)) - - @staticmethod - def _try_timestamp(string): - - try: - return float(string) - except Exception: - return None - @staticmethod def _choice_re(choices, flags=0): return re.compile(r"({})".format("|".join(choices)), flags=flags) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index d4c3872fb..32034f263 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -319,17 +319,6 @@ def test_parse_subsecond_rounding(self): self.assertEqual(self.parser.parse(string, format), self.expected) self.assertEqual(self.parser.parse_iso(string), self.expected) - def test_map_lookup_keyerror(self): - - with self.assertRaises(parser.ParserError): - parser.DateTimeParser._map_lookup({"a": "1"}, "b") - - def test_try_timestamp(self): - - self.assertEqual(parser.DateTimeParser._try_timestamp("1.1"), 1.1) - self.assertEqual(parser.DateTimeParser._try_timestamp("1"), 1) - self.assertEqual(parser.DateTimeParser._try_timestamp("abc"), None) - class DateTimeParserRegexTests(Chai): def setUp(self): From dc2e6d251f33838afa5e5fb826ab0b9cefe22cb8 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 30 Jun 2019 17:41:55 -0400 Subject: [PATCH 197/649] Fixed bug with YY mapping to 20 --- arrow/parser.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index ea677d8f2..ff8b6a209 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -42,7 +42,7 @@ class DateTimeParser(object): _ONE_OR_TWO_DIGIT_RE = re.compile(r"\d{1,2}") _FOUR_DIGIT_RE = re.compile(r"\d{4}") _TWO_DIGIT_RE = re.compile(r"\d{2}") - _TZ_RE = re.compile(r"[+\-]?\d{2}:?(\d{2})?") + _TZ_RE = re.compile(r"[+\-]?\d{2}:?(\d{2})?|Z") _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") _BASE_INPUT_RE_MAP = { @@ -163,6 +163,8 @@ def parse_iso(self, string): formats = ["{}T{}".format(f, time_string) for f in formats] if has_time and has_tz: + # Add "Z" to format strings to indicate to _parse_tokens + # that a timezone needs to be parsed formats = ["{}Z".format(f) for f in formats] if space_divider: @@ -212,7 +214,7 @@ def _generate_pattern_re(self, fmt): final_fmt_pattern = "" split_fmt = fmt_pattern.split(r"\#") - # Due to the way Python splits, 'a' will always be longer + # Due to the way Python splits, 'split_fmt' will always be longer for i in range(len(split_fmt)): final_fmt_pattern += split_fmt[i] if i < len(escaped_data): @@ -270,6 +272,9 @@ def parse(self, string, fmt, from_parse_iso=False): ) raise ParserError + if "YY" in fmt_tokens and match.end() != len(string): + raise ParserError + parts = {} for token in fmt_tokens: if token == "Do": @@ -404,7 +409,7 @@ def parse(cls, string): if string == "local": tzinfo = tz.tzlocal() - elif string in ["utc", "UTC"]: + elif string in ["utc", "UTC", "Z"]: tzinfo = tz.tzutc() else: From eadd97b750f691b1f1870eb73a907d2f17d18a8e Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 1 Jul 2019 06:13:07 -0400 Subject: [PATCH 198/649] Fixed README to point to Travis master build (#603) --- README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 113a6d305..41b988ac8 100644 --- a/README.rst +++ b/README.rst @@ -1,29 +1,29 @@ Arrow - Better dates & times for Python ======================================= -.. image:: https://travis-ci.org/crsmithdev/arrow.svg - :alt: build status +.. image:: https://travis-ci.org/crsmithdev/arrow.svg?branch=master + :alt: Build Status :target: https://travis-ci.org/crsmithdev/arrow .. image:: https://codecov.io/github/crsmithdev/arrow/coverage.svg?branch=master - :target: https://codecov.io/github/crsmithdev/arrow :alt: Codecov + :target: https://codecov.io/github/crsmithdev/arrow .. image:: https://img.shields.io/pypi/v/arrow.svg + :alt: PyPI Version :target: https://pypi.python.org/pypi/arrow - :alt: arrow PyPI download .. image:: https://img.shields.io/pypi/pyversions/arrow.svg + :alt: Supported Python Versions :target: https://pypi.python.org/pypi/arrow - :alt: python versions .. image:: https://img.shields.io/pypi/l/arrow.svg + :alt: License :target: https://pypi.python.org/pypi/arrow - :alt: license .. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :alt: Code Style: Black :target: https://github.com/python/black - :alt: code style Documentation: `arrow.readthedocs.io `_ --------------------------------------------------------------------------------- From dd5b8553921a306c86166bc19838c272c836b0e3 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Thu, 4 Jul 2019 21:05:59 +0100 Subject: [PATCH 199/649] Allow factory.get() to construct with kwargs --- arrow/factory.py | 9 +++++++++ tests/factory_tests.py | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/arrow/factory.py b/arrow/factory.py index 8591443a4..fa6f2fc0b 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -137,6 +137,15 @@ def get(self, *args, **kwargs): locale = kwargs.get("locale", "en_us") tz = kwargs.get("tzinfo", None) + # if kwargs given, send to constructor unless only tzinfo provided. + if len(kwargs) > 1: + arg_count = 3 + if len(kwargs) == 1: + if isinstance(tz, tzinfo): + pass + else: + arg_count = 3 + # () -> now, @ utc. if arg_count == 0: if isinstance(tz, tzinfo): diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 21e5ae4f6..e7e4c8870 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -221,6 +221,18 @@ def test_three_args(self): self.factory.get(2013, 1, 1), datetime(2013, 1, 1, tzinfo=tz.tzutc()) ) + def test_three_kwargs(self): + + self.assertEqual( + self.factory.get(year=2016, month=7, day=14), + datetime(2016, 7, 14, 0, 0, tzinfo=tz.tzutc()), + ) + + def test_insufficient_kwargs(self): + + with self.assertRaises(TypeError): + self.factory.get(year=2016) + class UtcNowTests(Chai): def setUp(self): From 955850c634ab5c2d470cdab150c27643c2872329 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Sat, 6 Jul 2019 15:52:35 +0100 Subject: [PATCH 200/649] Add tests for .get() warnings and comment on current problems --- arrow/parser.py | 62 ++++++++++++++++++++++++++++++++----------- tests/parser_tests.py | 33 ++++++++++++++++++++++- 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index ff8b6a209..c16be582a 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -20,17 +20,20 @@ class ParserError(RuntimeError): class GetParseWarning(DeprecationWarning): - """Raised when .get() is passed a string with no formats and matches incorrectly + """Raised when arrow.get() is passed a string with no formats and matches incorrectly on one of the default formats. e.g. arrow.get('blabla2016') -> arrow.get('13/4/2045') -> - In version 0.15.0 this will become a ParserError. + In version 0.15.0 this warning will become a ParserError. """ +warnings.simplefilter("default", GetParseWarning) + + class DateTimeParser(object): _FORMAT_RE = re.compile( @@ -113,9 +116,12 @@ def parse_iso(self, string): date_string, time_string = string.split("T", 1) # TODO: understand why we are not accounting for Z directly + # currently Z is ignored entirely but fromdatetime defaults to UTC, see arrow.py L196 + # '2013-02-03T04:05:06.78912Z' time_parts = re.split("[+-]", time_string, 1) colon_count = time_parts[0].count(":") + # TODO "20160504T010203Z" parses incorectly, time part is HH only, due to Z changing len is_basic_time_format = colon_count == 0 has_tz = len(time_parts) > 1 @@ -139,6 +145,8 @@ def parse_iso(self, string): if is_basic_time_format: time_string = time_string.replace(":", "") + # IDEA reduced set of date formats for basic + # TODO: add tests for all the new formats # required date formats to test against formats = [ @@ -156,7 +164,7 @@ def parse_iso(self, string): "YYYY/MM", "YYYY.MM", "YYYY", - "YY", + # "YY", this is not a good format to try by default? ] if has_time: @@ -241,39 +249,63 @@ def parse(self, string, fmt, from_parse_iso=False): # Accounts for cases such as "blahblah2016" if match.start() != 0: warnings.warn( - "Parser loosely matched {fmt} on '{string}', in the " - "future this will raise a ParserError.".format( + "Parser loosely matched {fmt} on '{string}', in version " + "0.15.0 this will raise a ParserError.".format( fmt=fmt, string=string ), category=GetParseWarning, ) - raise ParserError + # raise ParserError + # TODO arrow.get('2013-02-03 04:05:06.78912Z') is warning incorrectly due to this # Accounts for cases such as "2016-05T04:05:06.78912blahZ" if string[-1] == "Z" and match.end() != len(string) - 1: - # TODO what about 2019-06-24T10:45:31Z warnings.warn( - "Parser loosely matched {fmt} on '{string}', in the " - "future this will raise a ParserError.".format( + "Parser loosely matched {fmt} on '{string}', in version " + "0.15.0 this will raise a ParserError.".format( fmt=fmt, string=string ), category=GetParseWarning, ) - raise ParserError + # raise ParserError # Accounts for cases such as "2016-05T04:05:06.78912Zblah" if string[-1] != "Z" and match.end() != len(string): warnings.warn( - "Parser loosely matched {fmt} on '{string}', in the " - "future this will raise a ParserError.".format( + "Parser loosely matched {fmt} on '{string}', in version " + "0.15.0 this will raise a ParserError.".format( fmt=fmt, string=string ), category=GetParseWarning, ) - raise ParserError + # raise ParserError - if "YY" in fmt_tokens and match.end() != len(string): - raise ParserError + else: + # fixes arrow.get("15/01/2019", ["D/M/YY","D/M/YYYY"]) => + # FIXME arrow.get("Call 01-02-03 on 79-01-01 12:05:10", "YY-MM-DD HH:mm:ss") warns incorrectly + # FIXME arrow.get("79-01-01 12:05:10", "YY-MM-DD HH:mm:ss") warns incorrectly + # IDEA test for whitespace on either side of match? + if "YY" in fmt_tokens and match.start != 0 or match.end() != len(string): + warnings.warn( + "Parser loosely matched {fmt} on '{string}', in version " + "0.15.0 this will raise a ParserError.".format( + fmt=fmt, string=string + ), + category=GetParseWarning, + ) + # #raise ParserError + + if fmt == "YYYY": + # accounts for arrow.get('05/02/2017', ['YYYY', 'MM/DD/YYYY']) + if match.start() != 0 or match.end() != len(string): + warnings.warn( + "Parser loosely matched {fmt} on '{string}', in version " + "0.15.0 this will raise a ParserError.".format( + fmt=fmt, string=string + ), + category=GetParseWarning, + ) + # #raise ParserError parts = {} for token in fmt_tokens: diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 32034f263..c2d33b8b5 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -9,7 +9,7 @@ from dateutil import tz from arrow import parser -from arrow.parser import DateTimeParser, ParserError +from arrow.parser import DateTimeParser, GetParseWarning, ParserError class DateTimeParserTests(Chai): @@ -553,11 +553,42 @@ def test_YYYY_MM_DDTHH_mm_ss_SZ(self): # TODO: Shouldn't the datetime object being compared to have a tz offset of 0? # Test fails if this offset is added. + # fromdatetime adds UTC timezone on afterwards! parse_iso returns naive datetime in this case self.assertEqual( self.parser.parse_iso("2013-02-03 04:05:06.78912Z"), datetime(2013, 2, 3, 4, 5, 6, 789120), ) + def test_bad_get_parsing(self): + # fixes for loose get parsing + + with self.assertWarns(GetParseWarning): + self.parser.parse_iso("blabla2016") + + with self.assertWarns(GetParseWarning): + self.parser.parse_iso("2016blabla") + + with self.assertWarns(GetParseWarning): + self.parser.parse_iso("10/4/2045") + + with self.assertWarns(GetParseWarning): + self.parser.parse_iso("2016-05T04:05:06.78912blahZ") + + with self.assertWarns(GetParseWarning): + self.parser.parse_iso("2016-05T04:05:06.78912Zblah") + + with self.assertWarns(GetParseWarning): + self.parser.parse("15/01/2019", ["D/M/YY", "D/M/YYYY"]) + + with self.assertWarns(GetParseWarning): + self.parser.parse("05/02/2017", ["YYYY", "MM/DD/YYYY"]) + + with self.assertWarns(GetParseWarning): + self.parser.parse("1919/05/23", ["YY/M/D", "YYYY/M/D"]) + + with self.assertWarns(GetParseWarning): + self.parser.parse("2017/05/22", ["YYYY", "YYYY/MM/DD"]) + def test_gnu_date(self): """ regression tests for parsing output from GNU date(1) From 91d99d765d008001da113ab59280b5be8f36ab61 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 6 Jul 2019 18:23:13 -0500 Subject: [PATCH 201/649] Added comments and tests --- arrow/factory.py | 13 +++++---- arrow/parser.py | 26 +++++++++++++++--- tests/parser_tests.py | 63 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 11 deletions(-) diff --git a/arrow/factory.py b/arrow/factory.py index d6bf4dffa..0192412bc 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -59,7 +59,7 @@ def get(self, *args, **kwargs): >>> arrow.get(arw) - **One** ``str``, ``float``, or ``int``, convertible to a floating-point timestamp, to get + **One** ``float`` or ``int``, convertible to a floating-point timestamp, to get that timestamp in UTC:: >>> arrow.get(1367992474.293378) @@ -68,17 +68,16 @@ def get(self, *args, **kwargs): >>> arrow.get(1367992474) - >>> arrow.get('1367992474.293378') - - - >>> arrow.get('1367992474') - - **One** ISO-8601-formatted ``str``, to parse it:: >>> arrow.get('2013-09-29T01:26:43.830580') + **One** ISO-8601-formatted ``str``, in basic format, to parse it:: + + >>> arrow.get('20160413T133656.456289') + + **One** ``tzinfo``, to get the current time **converted** to that timezone:: >>> arrow.get(tz.tzlocal()) diff --git a/arrow/parser.py b/arrow/parser.py index ff8b6a209..bc5bc36d8 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -67,7 +67,6 @@ class DateTimeParser(object): "S": _ONE_OR_MORE_DIGIT_RE, } - MARKERS = ["YYYY", "MM", "DD"] SEPARATORS = ["-", "/", "."] def __init__(self, locale="en_us", cache_size=0): @@ -122,10 +121,10 @@ def parse_iso(self, string): has_hours = colon_count == 0 or len(time_string) == 2 has_minutes = colon_count == 1 or len(time_string) == 4 has_seconds = colon_count == 2 or len(time_string) == 6 - has_sub_seconds = re.search("[.,]", time_parts[0]) + has_subseconds = re.search("[.,]", time_parts[0]) - if has_sub_seconds: - time_string = "HH:mm:ss{}S".format(has_sub_seconds.group()) + if has_subseconds: + time_string = "HH:mm:ss{}S".format(has_subseconds.group()) elif has_seconds: time_string = "HH:mm:ss" elif has_minutes: @@ -272,9 +271,28 @@ def parse(self, string, fmt, from_parse_iso=False): ) raise ParserError + # Fixes bug where "15/01/2019" matches to "D/M/YY" + # arrow.get("15/01/2019", ["D/M/YY", "D/M/YYYY"]) if "YY" in fmt_tokens and match.end() != len(string): raise ParserError + # TODO: talk to Chris about these conditionals + # if string[-1] == "Z" and match.end() != len(string) - 1: + # # TODO: add an exception message + # raise ParserError + # + # if string[-1] != "Z" and match.end() != len(string): + # # TODO: add an exception message + # raise ParserError + # + # if match.start() != 0: + # # TODO: add an exception message + # raise ParserError + + # if ("YY" in fmt_tokens or "YYYY" in fmt_tokens) and (match.end() != len(string) or match.start() != 0): + # # TODO: add an exception message + # raise ParserError + parts = {} for token in fmt_tokens: if token == "Do": diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 32034f263..29bc86f42 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -115,6 +115,43 @@ def test_parser_multiple_line_caching(self): for _ in range(100): self.parser._generate_pattern_re("fmt_a") + def test_YY_and_YYYY_format_list(self): + + self.assertEqual( + self.parser.parse("15/01/19", ["D/M/YY", "D/M/YYYY"]), datetime(2019, 1, 15) + ) + + # Regression test for issue #580 + self.assertEqual( + self.parser.parse("15/01/2019", ["D/M/YY", "D/M/YYYY"]), + datetime(2019, 1, 15), + ) + + self.assertEqual( + 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()), + ) + + def test_long_year_input(self): + + # TODO: ask Chris if this should throw a ParserError + # Pendulum does not throw an error + self.assertEqual( + self.parser.parse("09 January 123456789101112", "DD MMMM YYYY"), + datetime(1234, 1, 9), + ) + + # Pendulum throws an error + with self.assertRaises(ParserError): + self.parser.parse("123456789101112 09 January", "YYYY DD MMMM") + + # Pendulum throws an error + with self.assertRaises(ParserError): + self.parser.parse("68096653015/01/19", "YY/M/DD") + class DateTimeParserParseTests(Chai): def setUp(self): @@ -584,6 +621,32 @@ def test_isoformat(self): self.assertEqual(self.parser.parse_iso(dt.isoformat()), dt) + def test_iso8601_string_with_extra_words_at_start_and_end(self): + + with self.assertRaises(ParserError): + self.parser.parse_iso("2016-05blah") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2016-05-16blah") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2016-05T04:05:06.78912ZblahZ") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2016-05T04:05:06.78912Zblah") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2016-05T04:05:06.78912blahZ") + + with self.assertRaises(ParserError): + self.parser.parse_iso("blah2016") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2016blah") + + with self.assertRaises(ParserError): + self.parser.parse_iso("blah2016blah") + class TzinfoParserTests(Chai): def setUp(self): From 6ab1c0ba646af93091232e67cc3fa1d3a6a93107 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 7 Jul 2019 14:57:41 -0400 Subject: [PATCH 202/649] Fixed merging issues --- Makefile | 2 +- arrow/parser.py | 20 ++++++------ tests/parser_tests.py | 71 ++++++++++++++++++++++--------------------- 3 files changed, 47 insertions(+), 46 deletions(-) diff --git a/Makefile b/Makefile index ec29f2aea..4852d5520 100644 --- a/Makefile +++ b/Makefile @@ -31,4 +31,4 @@ docs: clean: rm -rf venv .tox ./**/__pycache__ rm -rf dist build .egg arrow.egg-info - rm -f ./**/*.pyc .coverage + rm -f ./**/*.pyc ./**/.coverage diff --git a/arrow/parser.py b/arrow/parser.py index 489cd49c8..cec107171 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -254,7 +254,7 @@ def parse(self, string, fmt, from_parse_iso=False): ), category=GetParseWarning, ) - # raise ParserError + raise ParserError # TODO arrow.get('2013-02-03 04:05:06.78912Z') is warning incorrectly due to this # Accounts for cases such as "2016-05T04:05:06.78912blahZ" @@ -266,7 +266,7 @@ def parse(self, string, fmt, from_parse_iso=False): ), category=GetParseWarning, ) - # raise ParserError + raise ParserError # Accounts for cases such as "2016-05T04:05:06.78912Zblah" if string[-1] != "Z" and match.end() != len(string): @@ -277,12 +277,7 @@ def parse(self, string, fmt, from_parse_iso=False): ), category=GetParseWarning, ) - # raise ParserError - - # Fixes bug where "15/01/2019" matches to "D/M/YY" - # arrow.get("15/01/2019", ["D/M/YY", "D/M/YYYY"]) - if "YY" in fmt_tokens and match.end() != len(string): - raise ParserError + raise ParserError else: # fixes arrow.get("15/01/2019", ["D/M/YY","D/M/YYYY"]) => # FIXME arrow.get("Call 01-02-03 on 79-01-01 12:05:10", "YY-MM-DD HH:mm:ss") warns incorrectly @@ -296,7 +291,7 @@ def parse(self, string, fmt, from_parse_iso=False): ), category=GetParseWarning, ) - # #raise ParserError + raise ParserError if fmt == "YYYY": # accounts for arrow.get('05/02/2017', ['YYYY', 'MM/DD/YYYY']) @@ -308,7 +303,12 @@ def parse(self, string, fmt, from_parse_iso=False): ), category=GetParseWarning, ) - # #raise ParserError + raise ParserError + + # Fixes bug where "15/01/2019" matches to "D/M/YY" + # arrow.get("15/01/2019", ["D/M/YY", "D/M/YYYY"]) + if "YY" in fmt_tokens and match.end() != len(string): + raise ParserError # TODO: talk to Chris about these conditionals # if string[-1] == "Z" and match.end() != len(string) - 1: diff --git a/tests/parser_tests.py b/tests/parser_tests.py index c505645eb..deec9a09d 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -9,7 +9,7 @@ from dateutil import tz from arrow import parser -from arrow.parser import DateTimeParser, GetParseWarning, ParserError +from arrow.parser import DateTimeParser, ParserError class DateTimeParserTests(Chai): @@ -118,12 +118,13 @@ def test_parser_multiple_line_caching(self): def test_YY_and_YYYY_format_list(self): self.assertEqual( - self.parser.parse("15/01/19", ["D/M/YY", "D/M/YYYY"]), datetime(2019, 1, 15) + self.parser.parse("15/01/19", ["DD/MM/YY", "DD/MM/YYYY"]), + datetime(2019, 1, 15), ) # Regression test for issue #580 self.assertEqual( - self.parser.parse("15/01/2019", ["D/M/YY", "D/M/YYYY"]), + self.parser.parse("15/01/2019", ["DD/MM/YY", "DD/MM/YYYY"]), datetime(2019, 1, 15), ) @@ -588,43 +589,43 @@ def test_YYYY_MM_DDTHH_mm_ss_SZ(self): datetime(2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzoffset(None, 3600)), ) - # TODO: Shouldn't the datetime object being compared to have a tz offset of 0? - # Test fails if this offset is added. - # fromdatetime adds UTC timezone on afterwards! parse_iso returns naive datetime in this case + # parse_iso sets tzinfo to None if Z is passed, so a default datetime + # object is sufficient to compare against. + # Arrow adds +00:00 when get() is called directly and tzinfo is None self.assertEqual( self.parser.parse_iso("2013-02-03 04:05:06.78912Z"), datetime(2013, 2, 3, 4, 5, 6, 789120), ) - def test_bad_get_parsing(self): - # fixes for loose get parsing - - with self.assertWarns(GetParseWarning): - self.parser.parse_iso("blabla2016") - - with self.assertWarns(GetParseWarning): - self.parser.parse_iso("2016blabla") - - with self.assertWarns(GetParseWarning): - self.parser.parse_iso("10/4/2045") - - with self.assertWarns(GetParseWarning): - self.parser.parse_iso("2016-05T04:05:06.78912blahZ") - - with self.assertWarns(GetParseWarning): - self.parser.parse_iso("2016-05T04:05:06.78912Zblah") - - with self.assertWarns(GetParseWarning): - self.parser.parse("15/01/2019", ["D/M/YY", "D/M/YYYY"]) - - with self.assertWarns(GetParseWarning): - self.parser.parse("05/02/2017", ["YYYY", "MM/DD/YYYY"]) - - with self.assertWarns(GetParseWarning): - self.parser.parse("1919/05/23", ["YY/M/D", "YYYY/M/D"]) - - with self.assertWarns(GetParseWarning): - self.parser.parse("2017/05/22", ["YYYY", "YYYY/MM/DD"]) + # def test_bad_get_parsing(self): + # # fixes for loose get parsing + # + # with self.assertWarns(GetParseWarning): + # self.parser.parse_iso("blabla2016") + # + # with self.assertWarns(GetParseWarning): + # self.parser.parse_iso("2016blabla") + # + # with self.assertWarns(GetParseWarning): + # self.parser.parse_iso("10/4/2045") + # + # with self.assertWarns(GetParseWarning): + # self.parser.parse_iso("2016-05T04:05:06.78912blahZ") + # + # with self.assertWarns(GetParseWarning): + # self.parser.parse_iso("2016-05T04:05:06.78912Zblah") + # + # with self.assertWarns(GetParseWarning): + # self.parser.parse("15/01/2019", ["D/M/YY", "D/M/YYYY"]) + # + # with self.assertWarns(GetParseWarning): + # self.parser.parse("05/02/2017", ["YYYY", "MM/DD/YYYY"]) + # + # with self.assertWarns(GetParseWarning): + # self.parser.parse("1919/05/23", ["YY/M/D", "YYYY/M/D"]) + # + # with self.assertWarns(GetParseWarning): + # self.parser.parse("2017/05/22", ["YYYY", "YYYY/MM/DD"]) def test_gnu_date(self): """ From e3126c77b24e6d6132be3faa169c3815bd011144 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 7 Jul 2019 23:54:03 -0400 Subject: [PATCH 203/649] Fixed a number of parsing issues with a couple of regex tweaks and additions --- arrow/parser.py | 114 ++++++++++-------------------------------- tests/parser_tests.py | 73 +++++++++++++-------------- 2 files changed, 60 insertions(+), 127 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index cec107171..d04ecabb2 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -47,6 +47,7 @@ class DateTimeParser(object): _TWO_DIGIT_RE = re.compile(r"\d{2}") _TZ_RE = re.compile(r"[+\-]?\d{2}:?(\d{2})?|Z") _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") + _TIMESTAMP_RE = re.compile(r"\d+") _BASE_INPUT_RE_MAP = { "YYYY": _FOUR_DIGIT_RE, @@ -63,7 +64,7 @@ class DateTimeParser(object): "m": _ONE_OR_TWO_DIGIT_RE, "ss": _TWO_DIGIT_RE, "s": _ONE_OR_TWO_DIGIT_RE, - "X": re.compile(r"\d+"), + "X": _TIMESTAMP_RE, "ZZZ": _TZ_NAME_RE, "ZZ": _TZ_RE, "Z": _TZ_RE, @@ -101,9 +102,14 @@ def __init__(self, locale="en_us", cache_size=0): self._generate_pattern_re ) + # TODO: since we support more than ISO-8601, we should rename this function def parse_iso(self, string): + # TODO: account for more than 1 space like arrow.get(" 2016") + # string = string.strip() - has_time = "T" in string or " " in string.strip() + has_space_divider = " " in string and len(string.strip().split(" ")) == 2 + + has_time = "T" in string or has_space_divider space_divider = " " in string.strip() has_tz = False @@ -120,11 +126,11 @@ def parse_iso(self, string): time_parts = re.split("[+-]", time_string, 1) colon_count = time_parts[0].count(":") - # TODO "20160504T010203Z" parses incorectly, time part is HH only, due to Z changing len + # TODO "20160504T010203Z" parses incorrectly, time part is HH only, due to Z changing len is_basic_time_format = colon_count == 0 has_tz = len(time_parts) > 1 - has_hours = colon_count == 0 or len(time_string) == 2 + has_hours = len(time_string) == 2 has_minutes = colon_count == 1 or len(time_string) == 4 has_seconds = colon_count == 2 or len(time_string) == 6 has_subseconds = re.search("[.,]", time_parts[0]) @@ -146,7 +152,7 @@ def parse_iso(self, string): # IDEA reduced set of date formats for basic - # TODO: add tests for all the new formats + # TODO: add tests for all the new formats, especially basic format # required date formats to test against formats = [ "YYYY-MM-DD", @@ -227,6 +233,21 @@ def _generate_pattern_re(self, fmt): if i < len(escaped_data): final_fmt_pattern += escaped_data[i][1:-1] + # Wrap final_fmt_pattern in a custom word boundary to strictly + # match the formatting pattern and filter out date and time formats + # that include junk such as: blah1998-09-12 blah, blah 1998-09-12blah, + # blah1998-09-12blah. The custom word boundary matches every character + # that is not a whitespace character to allow for searching for a date + # and time string in a natural language sentence. Therefore, searching + # for a string of the form YYYY-MM-DD in "blah 1998-09-12 blah" will + # work properly. + # Reference: https://stackoverflow.com/q/14232931/3820660 + starting_word_boundary = r"(? - # FIXME arrow.get("Call 01-02-03 on 79-01-01 12:05:10", "YY-MM-DD HH:mm:ss") warns incorrectly - # FIXME arrow.get("79-01-01 12:05:10", "YY-MM-DD HH:mm:ss") warns incorrectly - # IDEA test for whitespace on either side of match? - if "YY" in fmt_tokens and match.start != 0 or match.end() != len(string): - warnings.warn( - "Parser loosely matched {fmt} on '{string}', in version " - "0.15.0 this will raise a ParserError.".format( - fmt=fmt, string=string - ), - category=GetParseWarning, - ) - raise ParserError - - if fmt == "YYYY": - # accounts for arrow.get('05/02/2017', ['YYYY', 'MM/DD/YYYY']) - if match.start() != 0 or match.end() != len(string): - warnings.warn( - "Parser loosely matched {fmt} on '{string}', in version " - "0.15.0 this will raise a ParserError.".format( - fmt=fmt, string=string - ), - category=GetParseWarning, - ) - raise ParserError - - # Fixes bug where "15/01/2019" matches to "D/M/YY" - # arrow.get("15/01/2019", ["D/M/YY", "D/M/YYYY"]) - if "YY" in fmt_tokens and match.end() != len(string): - raise ParserError - - # TODO: talk to Chris about these conditionals - # if string[-1] == "Z" and match.end() != len(string) - 1: - # # TODO: add an exception message - # raise ParserError - # - # if string[-1] != "Z" and match.end() != len(string): - # # TODO: add an exception message - # raise ParserError - # - # if match.start() != 0: - # # TODO: add an exception message - # raise ParserError - - # if ("YY" in fmt_tokens or "YYYY" in fmt_tokens) and (match.end() != len(string) or match.start() != 0): - # # TODO: add an exception message - # raise ParserError - parts = {} for token in fmt_tokens: if token == "Do": diff --git a/tests/parser_tests.py b/tests/parser_tests.py index deec9a09d..f1cd3229f 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -136,23 +136,6 @@ def test_YY_and_YYYY_format_list(self): datetime(2019, 1, 15, 4, 5, 6, 789120, tzinfo=tz.tzutc()), ) - def test_long_year_input(self): - - # TODO: ask Chris if this should throw a ParserError - # Pendulum does not throw an error - self.assertEqual( - self.parser.parse("09 January 123456789101112", "DD MMMM YYYY"), - datetime(1234, 1, 9), - ) - - # Pendulum throws an error - with self.assertRaises(ParserError): - self.parser.parse("123456789101112 09 January", "YYYY DD MMMM") - - # Pendulum throws an error - with self.assertRaises(ParserError): - self.parser.parse("68096653015/01/19", "YY/M/DD") - class DateTimeParserParseTests(Chai): def setUp(self): @@ -357,6 +340,17 @@ def test_parse_subsecond_rounding(self): self.assertEqual(self.parser.parse(string, format), self.expected) self.assertEqual(self.parser.parse_iso(string), self.expected) + # Regression tests for issue #560 + def test_parse_long_year(self): + with self.assertRaises(ParserError): + self.parser.parse("09 January 123456789101112", "DD MMMM YYYY"), + + with self.assertRaises(ParserError): + self.parser.parse("123456789101112 09 January", "YYYY DD MMMM") + + with self.assertRaises(ParserError): + self.parser.parse("68096653015/01/19", "YY/M/DD") + class DateTimeParserRegexTests(Chai): def setUp(self): @@ -653,31 +647,32 @@ def test_isoformat(self): self.assertEqual(self.parser.parse_iso(dt.isoformat()), dt) - def test_iso8601_string_with_extra_words_at_start_and_end(self): + def test_parse_with_extra_words_at_start_and_end(self): + input_format_pairs = [ + ("blah2016", "YYYY"), + ("blah2016blah", "YYYY"), + ("2016blah", "YYYY"), + ("2016-05blah", "YYYY-MM"), + ("2016-05-16blah", "YYYY-MM-DD"), + ("2016-05T04:05:06.789120blah", "YYYY-MM-DDThh:mm:ss.S"), + ("2016-05T04:05:06.789120ZblahZ", "YYYY-MM-DDThh:mm:ss.SZ"), + ("2016-05T04:05:06.789120Zblah", "YYYY-MM-DDThh:mm:ss.SZ"), + ("2016-05T04:05:06.789120blahZ", "YYYY-MM-DDThh:mm:ss.SZ"), + ] - with self.assertRaises(ParserError): - self.parser.parse_iso("2016-05blah") + for pair in input_format_pairs: + with self.assertRaises(ParserError): + self.parser.parse_iso(pair[0]) - with self.assertRaises(ParserError): - self.parser.parse_iso("2016-05-16blah") + with self.assertRaises(ParserError): + self.parser.parse(pair[0], pair[1]) - with self.assertRaises(ParserError): - self.parser.parse_iso("2016-05T04:05:06.78912ZblahZ") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2016-05T04:05:06.78912Zblah") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2016-05T04:05:06.78912blahZ") - - with self.assertRaises(ParserError): - self.parser.parse_iso("blah2016") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2016blah") - - with self.assertRaises(ParserError): - self.parser.parse_iso("blah2016blah") + # Spaces surrounding the parsable date are ok because we + # allow the parsing of natural language input + self.assertEqual(self.parser.parse_iso("blah 2016 blah"), datetime(2016, 1, 1)) + self.assertEqual( + self.parser.parse("blah 2016 blah", "YYYY"), datetime(2016, 1, 1) + ) class TzinfoParserTests(Chai): From f77e6984ffe5b088fa30b8226d6590bbdc0bc1ba Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 8 Jul 2019 01:09:55 -0400 Subject: [PATCH 204/649] Added unit tests and added cleanup/validation to datetimte string in parse_iso --- arrow/parser.py | 129 ++++++++++++++++++++++-------------------- tests/parser_tests.py | 74 +++++++++++++++++++++--- 2 files changed, 135 insertions(+), 68 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index d04ecabb2..d9ac5bee3 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -103,22 +103,51 @@ def __init__(self, locale="en_us", cache_size=0): ) # TODO: since we support more than ISO-8601, we should rename this function - def parse_iso(self, string): - # TODO: account for more than 1 space like arrow.get(" 2016") - # string = string.strip() + def parse_iso(self, datetime_string): + # TODO: talk to Chris about this => the below space divider checks + # are not really necessary thanks to the new regex changes, but I think + # it is good to include them to provide better error messages. - has_space_divider = " " in string and len(string.strip().split(" ")) == 2 + # strip leading and trailing whitespace + datetime_string = datetime_string.strip() - has_time = "T" in string or has_space_divider - space_divider = " " in string.strip() + has_space_divider = " " in datetime_string + num_space_dividers = len(datetime_string.split(" ")) + if has_space_divider and num_space_dividers != 2: + raise ParserError( + "Expected 1 space divider, but was given {}. Try passing in a format string to resolve this.".format( + num_space_dividers + ) + ) + + has_time = has_space_divider or "T" in datetime_string has_tz = False + # TODO: add tests for all the new formats, especially basic format + # required date formats to test against + formats = [ + "YYYY-MM-DD", + "YYYY-M-DD", + "YYYY-M-D", + "YYYY/MM/DD", + "YYYY/M/DD", + "YYYY/M/D", + "YYYY.MM.DD", + "YYYY.M.DD", + "YYYY.M.D", + "YYYYMMDD", + "YYYY-MM", + "YYYY/MM", + "YYYY.MM", + "YYYY", + ] + if has_time: - if space_divider: - date_string, time_string = string.split(" ", 1) + if has_space_divider: + date_string, time_string = datetime_string.split(" ", 1) else: - date_string, time_string = string.split("T", 1) + date_string, time_string = datetime_string.split("T", 1) # TODO: understand why we are not accounting for Z directly # currently Z is ignored entirely but fromdatetime defaults to UTC, see arrow.py L196 @@ -150,40 +179,45 @@ def parse_iso(self, string): if is_basic_time_format: time_string = time_string.replace(":", "") - # IDEA reduced set of date formats for basic + if has_space_divider: + formats = ["{} {}".format(f, time_string) for f in formats] + else: + formats = ["{}T{}".format(f, time_string) for f in formats] - # TODO: add tests for all the new formats, especially basic format - # required date formats to test against - formats = [ - "YYYY-MM-DD", - "YYYY-M-DD", - "YYYY-M-D", - "YYYY/MM/DD", - "YYYY/M/DD", - "YYYY/M/D", - "YYYY.MM.DD", - "YYYY.M.DD", - "YYYY.M.D", - "YYYYMMDD", - "YYYY-MM", - "YYYY/MM", - "YYYY.MM", - "YYYY", - # "YY", this is not a good format to try by default? - ] - - if has_time: - formats = ["{}T{}".format(f, time_string) for f in formats] + # TODO: reduce set of date formats for basic? if has_time and has_tz: # Add "Z" to format strings to indicate to _parse_tokens # that a timezone needs to be parsed formats = ["{}Z".format(f) for f in formats] - if space_divider: - formats = [item.replace("T", " ", 1) for item in formats] + # TODO: make thrown error messages less cryptic and more informative + return self._parse_multiformat(datetime_string, formats, True) + + def parse(self, datetime_string, fmt, from_parse_iso=False): + + if isinstance(fmt, list): + return self._parse_multiformat(datetime_string, fmt) + + fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt) - return self._parse_multiformat(string, formats, True) + match = fmt_pattern_re.search(datetime_string) + if match is None: + raise ParserError( + "Failed to match '{}' when parsing '{}'".format( + fmt_pattern_re.pattern, datetime_string + ) + ) + + parts = {} + for token in fmt_tokens: + if token == "Do": + value = match.group("value") + else: + value = match.group(token) + self._parse_token(token, value, parts) + + return self._build_datetime(parts) def _generate_pattern_re(self, fmt): @@ -250,31 +284,6 @@ def _generate_pattern_re(self, fmt): return tokens, re.compile(final_fmt_pattern, flags=re.IGNORECASE) - def parse(self, string, fmt, from_parse_iso=False): - - if isinstance(fmt, list): - return self._parse_multiformat(string, fmt) - - fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt) - - match = fmt_pattern_re.search(string) - if match is None: - raise ParserError( - "Failed to match '{}' when parsing '{}'".format( - fmt_pattern_re.pattern, string - ) - ) - - parts = {} - for token in fmt_tokens: - if token == "Do": - value = match.group("value") - else: - value = match.group(token) - self._parse_token(token, value, parts) - - return self._build_datetime(parts) - def _parse_token(self, token, value, parts): if token == "YYYY": diff --git a/tests/parser_tests.py b/tests/parser_tests.py index f1cd3229f..c1679e245 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -647,33 +647,91 @@ def test_isoformat(self): self.assertEqual(self.parser.parse_iso(dt.isoformat()), dt) - def test_parse_with_extra_words_at_start_and_end(self): + def test_parse_with_extra_words_at_start_and_end_invalid(self): + # The tuple's second entry is None if the datetime string + # is valid when a format string is passed in input_format_pairs = [ ("blah2016", "YYYY"), ("blah2016blah", "YYYY"), + ("blah 2016 blah", None), ("2016blah", "YYYY"), ("2016-05blah", "YYYY-MM"), ("2016-05-16blah", "YYYY-MM-DD"), - ("2016-05T04:05:06.789120blah", "YYYY-MM-DDThh:mm:ss.S"), - ("2016-05T04:05:06.789120ZblahZ", "YYYY-MM-DDThh:mm:ss.SZ"), - ("2016-05T04:05:06.789120Zblah", "YYYY-MM-DDThh:mm:ss.SZ"), - ("2016-05T04:05:06.789120blahZ", "YYYY-MM-DDThh:mm:ss.SZ"), + ("2016-05-16T04:05:06.789120blah", "YYYY-MM-DDThh:mm:ss.S"), + ("2016-05-16T04:05:06.789120ZblahZ", "YYYY-MM-DDThh:mm:ss.SZ"), + ("2016-05-16T04:05:06.789120Zblah", "YYYY-MM-DDThh:mm:ss.SZ"), + ("2016-05-16T04:05:06.789120blahZ", "YYYY-MM-DDThh:mm:ss.SZ"), + ("Meet me at 2016-05-16T04:05:06.789120 on Tuesday", None), + ("Meet me at 2016-05-16 04:05:06.789120 on Tuesday", None), ] for pair in input_format_pairs: with self.assertRaises(ParserError): self.parser.parse_iso(pair[0]) - with self.assertRaises(ParserError): - self.parser.parse(pair[0], pair[1]) + if pair[1] is not None: + with self.assertRaises(ParserError): + self.parser.parse(pair[0], pair[1]) + def test_parse_with_extra_words_at_start_and_end_valid(self): # Spaces surrounding the parsable date are ok because we # allow the parsing of natural language input - self.assertEqual(self.parser.parse_iso("blah 2016 blah"), datetime(2016, 1, 1)) self.assertEqual( self.parser.parse("blah 2016 blah", "YYYY"), datetime(2016, 1, 1) ) + self.assertEqual( + self.parser.parse( + "Meet me at 2016-05-16T04:05:06.789120 on Tuesday", + "YYYY-MM-DDThh:mm:ss.S", + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + self.assertEqual( + self.parser.parse( + "Meet me at 2016-05-16 04:05:06.789120 on Tuesday", + "YYYY-MM-DD hh:mm:ss.S", + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + def test_parse_with_leading_and_trailing_whitespace(self): + self.assertEqual(self.parser.parse_iso(" 2016"), datetime(2016, 1, 1)) + self.assertEqual(self.parser.parse(" 2016", "YYYY"), datetime(2016, 1, 1)) + + self.assertEqual(self.parser.parse_iso("2016 "), datetime(2016, 1, 1)) + self.assertEqual(self.parser.parse("2016 ", "YYYY"), datetime(2016, 1, 1)) + + self.assertEqual( + self.parser.parse_iso(" 2016 "), datetime(2016, 1, 1) + ) + self.assertEqual( + self.parser.parse(" 2016 ", "YYYY"), datetime(2016, 1, 1) + ) + + self.assertEqual( + self.parser.parse_iso(" 2016-05-16 04:05:06.789120 "), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + self.assertEqual( + self.parser.parse( + " 2016-05-16 04:05:06.789120 ", "YYYY-MM-DD hh:mm:ss.S" + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + self.assertEqual( + self.parser.parse_iso(" 2016-05-16T04:05:06.789120 "), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + self.assertEqual( + self.parser.parse( + " 2016-05-16T04:05:06.789120 ", "YYYY-MM-DDThh:mm:ss.S" + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + class TzinfoParserTests(Chai): def setUp(self): From 24927fa47d82f01809e8498be9fa25b8e1b62838 Mon Sep 17 00:00:00 2001 From: Ashwin Easo Date: Tue, 9 Jul 2019 05:59:08 +0530 Subject: [PATCH 205/649] Replace doc theme with alabaster. (#605) --- docs/_themes/COPYING.txt | 14 - docs/_themes/README.rst | 32 --- docs/_themes/f6/NEWS.txt | 7 - docs/_themes/f6/README.rst | 31 --- docs/_themes/f6/layout.html | 41 --- docs/_themes/f6/static/brillant.png | Bin 85 -> 0 bytes docs/_themes/f6/static/f6.css | 389 ---------------------------- docs/_themes/f6/theme.conf | 3 - docs/conf.py | 214 +++++++++++++-- docs/make.bat | 35 +++ 10 files changed, 228 insertions(+), 538 deletions(-) delete mode 100644 docs/_themes/COPYING.txt delete mode 100644 docs/_themes/README.rst delete mode 100644 docs/_themes/f6/NEWS.txt delete mode 100644 docs/_themes/f6/README.rst delete mode 100644 docs/_themes/f6/layout.html delete mode 100644 docs/_themes/f6/static/brillant.png delete mode 100644 docs/_themes/f6/static/f6.css delete mode 100644 docs/_themes/f6/theme.conf create mode 100644 docs/make.bat diff --git a/docs/_themes/COPYING.txt b/docs/_themes/COPYING.txt deleted file mode 100644 index 069f469b8..000000000 --- a/docs/_themes/COPYING.txt +++ /dev/null @@ -1,14 +0,0 @@ -Copyright (c) 2011 Vimalkumar Velayudhan - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see diff --git a/docs/_themes/README.rst b/docs/_themes/README.rst deleted file mode 100644 index a112101d3..000000000 --- a/docs/_themes/README.rst +++ /dev/null @@ -1,32 +0,0 @@ -sphinx-themes -============= - -These are some themes for Python `Sphinx `_ -documentation projects. - -Preview -------- -To see how these themes look, visit http://vimalkumar.in/sphinx-themes - -Download --------- -Released versions are available from https://github.com/vkvn/sphinx-themes/downloads - -You can also download this repository as a `zip archive `_ - -Support -------- -If there are problems with any of these themes, you can file a bug report at -https://github.com/vkvn/sphinx-themes/issues - -Themes are licensed under the -`GNU General Public License `_. - - -.. raw:: html - - - - Endorse vkvn on Coderwall diff --git a/docs/_themes/f6/NEWS.txt b/docs/_themes/f6/NEWS.txt deleted file mode 100644 index 65238d901..000000000 --- a/docs/_themes/f6/NEWS.txt +++ /dev/null @@ -1,7 +0,0 @@ -News -==== - -1.0 ---- -* Release date: 2012-11-01 -* Initial release diff --git a/docs/_themes/f6/README.rst b/docs/_themes/f6/README.rst deleted file mode 100644 index 052b24b69..000000000 --- a/docs/_themes/f6/README.rst +++ /dev/null @@ -1,31 +0,0 @@ -f6 theme for Python Sphinx -========================== - -f6? ---- -A light theme for Python Sphinx documentation projects. Mostly white -> #ffffff -> f6 - -Preview -------- -http://vimalkumar.in/sphinx-themes/f6 - -Download --------- -Released versions are available from http://github.com/vkvn/sphinx-themes/downloads - -Installation ------------- -#. Extract the archive. -#. Modify ``conf.py`` of an existing Sphinx project or create new project using ``sphinx-quickstart``. -#. Change the ``html_theme`` parameter to ``f6``. -#. Change the ``html_theme_path`` to the location containing the extracted archive. - -License -------- -`GNU General Public License `_ - -Credits -------- -Modified from the default Sphinx theme -- Sphinxdoc - -Background pattern from http://subtlepatterns.com diff --git a/docs/_themes/f6/layout.html b/docs/_themes/f6/layout.html deleted file mode 100644 index 426be8542..000000000 --- a/docs/_themes/f6/layout.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends "basic/layout.html" %} - -{%- block doctype -%} - -{%- endblock -%} - -{%- block extrahead -%} - - -{%- endblock -%} - -{# put the sidebar before the body #} -{% block sidebarlogo %} -Fork me on GitHub - -

github.com/crsmithdev/arrow

- - -{% endblock%} -{% block sidebar1 %}{{ sidebar() }}{% endblock %} -{% block sidebar2 %}{% endblock %} -{%- block relbar1 %}{% endblock %} -{%- block footer %} - -{%- endblock %} diff --git a/docs/_themes/f6/static/brillant.png b/docs/_themes/f6/static/brillant.png deleted file mode 100644 index 82cf2d4957123beb8d6fbcc4cc097155f96389e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1SD^IDZKzvGM+AuAsn*FKR!P{|NZ^_|NsA= iXIRo8Bf#CE$iUFYE475nk@X@_9fPN tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = "" + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# "da", "de", "en", "es", "fi", "fr", "hu", "it", "ja" +# "nl", "no", "pt", "ro", "ru", "sv", "tr" +# html_search_language = "en" + +# A dictionary with options for the search language support, empty by default. +# Now only "ja" uses this config value +# html_search_options = {"type": "default"} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# html_search_scorer = "scorer.js" + +# Output file base name for HTML help builder. +htmlhelp_basename = "arrowdoc" + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ("letterpaper" or "a4paper"). + # "papersize": "letterpaper", + # The font size ("10pt", "11pt" or "12pt"). + # "pointsize": "10pt", + # Additional stuff for the LaTeX preamble. + # "preamble": "", + # Latex figure (float) alignment + # "figure_align": "htbp", +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, "arrow.tex", u"arrow Documentation", u"Chris Smith", "manual") +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(master_doc, "arrow", u"arrow Documentation", [author], 1)] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "arrow", + u"arrow Documentation", + author, + "arrow", + "One line description of project.", + "Miscellaneous", + ) +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project +epub_author = author +epub_publisher = author +epub_copyright = copyright + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ["search.html"] diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 000000000..27f573b87 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd From fdb77c51d5ec5e250eb1a5bb360252f02cd745eb Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 8 Jul 2019 21:04:39 -0400 Subject: [PATCH 206/649] Added table of contents, added badges to docs, and general docs cleanup --- Makefile | 1 - docs/Makefile | 7 +- docs/conf.py | 223 +++++-------------------------------------------- docs/index.rst | 24 ++++++ docs/make.bat | 4 +- 5 files changed, 50 insertions(+), 209 deletions(-) diff --git a/Makefile b/Makefile index ec29f2aea..2b534ea60 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,6 @@ lint: venv/bin/pre-commit run --all-files --show-diff-on-failure docs: - touch docs/index.rst . venv/bin/activate && cd docs; make html clean: diff --git a/docs/Makefile b/docs/Makefile index 51285967a..d4bb2cbb9 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,9 +1,10 @@ # Minimal makefile for Sphinx documentation # -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build diff --git a/docs/conf.py b/docs/conf.py index c4fcf9331..d682c868b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,236 +1,53 @@ # -*- coding: utf-8 -*- -# -# Configuration file for the Sphinx documentation builder. -# -# This file does only contain a selection of the most common options. For a -# full list see the documentation: -# http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# import os import sys sys.path.insert(0, os.path.abspath("..")) - # -- Project information ----------------------------------------------------- -project = u"arrow" -copyright = u"2019, Chris Smith" -author = u"Chris Smith" +project = "arrow" +copyright = "2019, Chris Smith" +author = "Chris Smith" -# The full version, including alpha/beta/rc tags -release = u"0.14.0" +release = "0.14.0" # -- General configuration --------------------------------------------------- -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = "1.0" - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named "sphinx.ext.*") or your custom -# ones. -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.intersphinx", - "sphinx.ext.todo", - "sphinx.ext.viewcode", -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = [".rst", ".md"] -source_suffix = ".rst" +extensions = ["sphinx.ext.autodoc"] -# The master toctree document. -master_doc = "index" +templates_path = [] -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] -# The reST default role (used for this markup: `text`) to use for all -# documents. -# default_role = None - -# If true, "()" will be appended to :func: etc. cross-reference text. -add_function_parentheses = False - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False +master_doc = "index" +source_suffix = ".rst" +pygments_style = "sphinx" -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True +language = None # -- Options for HTML output ------------------------------------------------- -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# html_theme = "alabaster" +html_theme_path = [] +html_static_path = [] + +html_show_sourcelink = False +html_show_sphinx = False +html_show_copyright = True -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = { - "show_powered_by": False, + "description": "Arrow is a sensible and human-friendly approach to dates, times, and timestamps.", "github_user": "crsmithdev", "github_repo": "arrow", "github_banner": True, "show_related": False, - "note_bg": "#FFF59C", + "show_powered_by": False, } -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = [] - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -html_use_smartypants = False - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# The default sidebars (for documents that don"t match any pattern) are -# defined by theme itself. Builtin themes are using these templates by -# default: ``["localtoc.html", "relations.html", "sourcelink.html", -# "searchbox.html"]``. -# -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -html_show_sourcelink = False - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -html_show_sphinx = False - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = "" - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# "da", "de", "en", "es", "fi", "fr", "hu", "it", "ja" -# "nl", "no", "pt", "ro", "ru", "sv", "tr" -# html_search_language = "en" - -# A dictionary with options for the search language support, empty by default. -# Now only "ja" uses this config value -# html_search_options = {"type": "default"} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -# html_search_scorer = "scorer.js" - -# Output file base name for HTML help builder. -htmlhelp_basename = "arrowdoc" - - -# -- Options for LaTeX output ------------------------------------------------ - -latex_elements = { - # The paper size ("letterpaper" or "a4paper"). - # "papersize": "letterpaper", - # The font size ("10pt", "11pt" or "12pt"). - # "pointsize": "10pt", - # Additional stuff for the LaTeX preamble. - # "preamble": "", - # Latex figure (float) alignment - # "figure_align": "htbp", +html_sidebars = { + "**": ["about.html", "localtoc.html", "relations.html", "searchbox.html"] } - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, "arrow.tex", u"arrow Documentation", u"Chris Smith", "manual") -] - - -# -- Options for manual page output ------------------------------------------ - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [(master_doc, "arrow", u"arrow Documentation", [author], 1)] - - -# -- Options for Texinfo output ---------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - "arrow", - u"arrow Documentation", - author, - "arrow", - "One line description of project.", - "Miscellaneous", - ) -] - - -# -- Options for Epub output ------------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = project -epub_author = author -epub_publisher = author -epub_copyright = copyright - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ["search.html"] diff --git a/docs/index.rst b/docs/index.rst index fd5376f1c..c5446b0d6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,6 +2,30 @@ Arrow: better dates and times for Python ========================================= +.. image:: https://travis-ci.org/crsmithdev/arrow.svg?branch=master + :alt: Build Status + :target: https://travis-ci.org/crsmithdev/arrow + +.. image:: https://codecov.io/github/crsmithdev/arrow/coverage.svg?branch=master + :alt: Codecov + :target: https://codecov.io/github/crsmithdev/arrow + +.. image:: https://img.shields.io/pypi/v/arrow.svg + :alt: PyPI Version + :target: https://pypi.python.org/pypi/arrow + +.. image:: https://img.shields.io/pypi/pyversions/arrow.svg + :alt: Supported Python Versions + :target: https://pypi.python.org/pypi/arrow + +.. image:: https://img.shields.io/pypi/l/arrow.svg + :alt: License + :target: https://pypi.python.org/pypi/arrow + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :alt: Code Style: Black + :target: https://github.com/python/black + ----- What? ----- diff --git a/docs/make.bat b/docs/make.bat index 27f573b87..2119f5109 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -25,11 +25,11 @@ if errorlevel 9009 ( exit /b 1 ) -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd From a9a43a7af2b74cfda6852f14a925e40d5af31a63 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 8 Jul 2019 21:22:28 -0400 Subject: [PATCH 207/649] Supplemented gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index cee28fdd6..723b75764 100644 --- a/.gitignore +++ b/.gitignore @@ -98,6 +98,7 @@ celerybeat-schedule env/ venv/ ENV/ +local/ env.bak/ venv.bak/ From 682f0ff686d74e314e7ff87be1ea6bd9eb6260ed Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 8 Jul 2019 23:02:02 -0400 Subject: [PATCH 208/649] Heavily refactored documentation to reduce duplication and streamline styles in RST --- .pre-commit-config.yaml | 8 +-- README.rst | 34 +++++----- arrow/__init__.py | 2 - arrow/_version.py | 2 + docs/conf.py | 7 +- docs/index.rst | 140 ++++++---------------------------------- setup.py | 15 ++--- tox.ini | 9 +-- 8 files changed, 62 insertions(+), 155 deletions(-) create mode 100644 arrow/_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e4e41268..242112843 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,11 +15,11 @@ repos: hooks: - id: seed-isort-config - repo: https://github.com/pre-commit/mirrors-isort - rev: v4.3.20 + rev: v4.3.21 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v1.18.0 + rev: v1.20.1 hooks: - id: pyupgrade - repo: https://github.com/pre-commit/pygrep-hooks @@ -31,9 +31,9 @@ repos: rev: 19.3b0 hooks: - id: black - args: [--safe, --quiet, --target-version=py27] + args: [--safe, --quiet] - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.7 + rev: 3.7.8 hooks: - id: flake8 additional_dependencies: [flake8-bugbear] diff --git a/README.rst b/README.rst index 41b988ac8..36550f0ac 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,7 @@ -Arrow - Better dates & times for Python -======================================= +Arrow: Better dates & times for Python +====================================== + +.. start-inclusion-marker-do-not-remove .. image:: https://travis-ci.org/crsmithdev/arrow.svg?branch=master :alt: Build Status @@ -25,9 +27,6 @@ Arrow - Better dates & times for Python :alt: Code Style: Black :target: https://github.com/python/black -Documentation: `arrow.readthedocs.io `_ ---------------------------------------------------------------------------------- - What? ----- @@ -38,22 +37,22 @@ Arrow is heavily inspired by `moment.js `_ a Why? ---- -Python's standard library and some other low-level modules have near-complete date, time and time zone functionality but don't work very well from a usability perspective: +Python's standard library and some other low-level modules have near-complete date, time and timezone functionality but don't work very well from a usability perspective: - Too many modules: datetime, time, calendar, dateutil, pytz and more - Too many types: date, time, datetime, tzinfo, timedelta, relativedelta, etc. -- Time zones and timestamp conversions are verbose and unpleasant -- Time zone naievety is the norm +- Timezones and timestamp conversions are verbose and unpleasant +- Timezone naivety is the norm - Gaps in functionality: ISO-8601 parsing, timespans, humanization Features -------- - Fully implemented, drop-in replacement for datetime -- Supports Python 2.7, 3.5, 3.6, 3.7, and 3.8 -- Time zone-aware & UTC by default +- Supports Python 2.7, 3.5, 3.6, 3.7 and 3.8 +- Timezone-aware & UTC by default - Provides super-simple creation options for many common input scenarios -- Updated .replace method with support for relative offsets, including weeks +- Updated :code:`replace` method with support for relative offsets, including weeks - Formats and parses strings automatically - Partial support for ISO-8601 - Timezone conversion @@ -66,7 +65,7 @@ Quick Start ----------- Installation -^^^^^^^^^^^^ +~~~~~~~~~~~~ To install Arrow, use `pip `_ or `pipenv `_: @@ -75,9 +74,9 @@ To install Arrow, use `pip `_ or `pip $ pip install -U arrow Example Usage -^^^^^^^^^^^^^ +~~~~~~~~~~~~~ -.. code-block:: pycon +.. code-block:: python >>> import arrow >>> utc = arrow.utcnow() @@ -110,7 +109,12 @@ Example Usage >>> local.humanize(locale='ko_kr') '1시간 전' -Further documentation can be found at `arrow.readthedocs.io `_. +.. end-inclusion-marker-do-not-remove + +Documentation +------------- + +For full documentation, please visit `arrow.readthedocs.io `_. Contributing ------------ diff --git a/arrow/__init__.py b/arrow/__init__.py index 7fad091bd..2a6d50339 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -2,5 +2,3 @@ from .api import get, now, utcnow from .arrow import Arrow from .factory import ArrowFactory - -__version__ = "0.14.2" diff --git a/arrow/_version.py b/arrow/_version.py new file mode 100644 index 000000000..1a8ad57d3 --- /dev/null +++ b/arrow/_version.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +__version__ = "0.14.2" diff --git a/docs/conf.py b/docs/conf.py index d682c868b..1e67b23c3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,18 +2,23 @@ # -- Path setup -------------------------------------------------------------- +import io import os import sys sys.path.insert(0, os.path.abspath("..")) +about = {} +with io.open("../arrow/_version.py", "r", encoding="utf-8") as f: + exec(f.read(), about) + # -- Project information ----------------------------------------------------- project = "arrow" copyright = "2019, Chris Smith" author = "Chris Smith" -release = "0.14.0" +release = about["__version__"] # -- General configuration --------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index c5446b0d6..5e171c2bb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,114 +1,17 @@ -========================================= -Arrow: better dates and times for Python -========================================= - -.. image:: https://travis-ci.org/crsmithdev/arrow.svg?branch=master - :alt: Build Status - :target: https://travis-ci.org/crsmithdev/arrow - -.. image:: https://codecov.io/github/crsmithdev/arrow/coverage.svg?branch=master - :alt: Codecov - :target: https://codecov.io/github/crsmithdev/arrow - -.. image:: https://img.shields.io/pypi/v/arrow.svg - :alt: PyPI Version - :target: https://pypi.python.org/pypi/arrow - -.. image:: https://img.shields.io/pypi/pyversions/arrow.svg - :alt: Supported Python Versions - :target: https://pypi.python.org/pypi/arrow - -.. image:: https://img.shields.io/pypi/l/arrow.svg - :alt: License - :target: https://pypi.python.org/pypi/arrow - -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :alt: Code Style: Black - :target: https://github.com/python/black - ------ -What? ------ - -Arrow is a Python library that offers a sensible, human-friendly approach to creating, manipulating, formatting and converting dates, times, and timestamps. It implements and updates the datetime type, plugging gaps in functionality, and provides an intelligent module API that supports many common creation scenarios. Simply put, it helps you work with dates and times with fewer imports and a lot less code. - -Arrow is heavily inspired by `moment.js `_ and `requests `_. - ----- -Why? ----- -Python's standard library and some other low-level modules have near-complete date, time and timezone functionality but don't work very well from a usability perspective: - -- Too many modules: datetime, time, calendar, dateutil, pytz and more -- Too many types: date, time, datetime, tzinfo, timedelta, relativedelta, etc. -- Timezones and timestamp conversions are verbose and unpleasant -- Timezone naivety is the norm -- Gaps in functionality: ISO-8601 parsing, time spans, humanization - --------- -Features --------- - -- Fully implemented, drop-in replacement for datetime -- Supports Python 2.7, 3.5, 3.6, 3.7 and 3.8 -- Timezone-aware & UTC by default -- Provides super-simple creation options for many common input scenarios -- Updated .replace method with support for relative offsets, including weeks -- Formats and parses strings automatically -- Partial ISO-8601 support -- Timezone conversion -- Timestamp available as a property -- Generates time spans, ranges, floors and ceilings in time frames from year to microsecond precision -- Humanizes and supports a growing list of contributed locales -- Extensible for your own Arrow-derived types - ----------- -Quickstart ----------- - -.. code-block:: bash - - $ pip install arrow +Arrow: Better dates & times for Python +====================================== -.. code-block:: python - - >>> import arrow - >>> utc = arrow.utcnow() - >>> utc - - - >>> utc = utc.shift(hours=-1) - >>> utc - - - >>> local = utc.to('US/Pacific') - >>> local - - - >>> arrow.get('2013-05-11T21:23:58.970460+00:00') - +Release v\ |release|. (`Installation`_) - >>> local.timestamp - 1368303838 +.. include:: ../README.rst + :start-after: start-inclusion-marker-do-not-remove + :end-before: end-inclusion-marker-do-not-remove - >>> local.format() - '2013-05-11 13:23:58 -07:00' - - >>> local.format('YYYY-MM-DD HH:mm:ss ZZ') - '2013-05-11 13:23:58 -07:00' - - >>> local.humanize() - 'an hour ago' - - >>> local.humanize(locale='ko_kr') - '1시간 전' - ------------- User's Guide ------------ Creation -======== +~~~~~~~~ Get 'now' easily: @@ -186,7 +89,7 @@ Arrow objects can be instantiated directly too, with the same arguments as a dat Properties -========== +~~~~~~~~~~ Get a datetime or timestamp representation: @@ -226,8 +129,8 @@ Call datetime functions that return properties: >>> a.time() datetime.time(4, 38, 15, 447644) -Replace & shift -=============== +Replace & Shift +~~~~~~~~~~~~~~~ Get a new :class:`Arrow ` object, with altered attributes, just as you would with a datetime: @@ -256,7 +159,7 @@ Even replace the timezone without altering other attributes: Format -====== +~~~~~~ .. code-block:: python @@ -264,7 +167,7 @@ Format '2013-05-07 05:23:16 -00:00' Convert -======= +~~~~~~~ Convert to timezones by name or tzinfo: @@ -292,7 +195,7 @@ Or using shorthand: Humanize -======== +~~~~~~~~ Humanize relative to now: @@ -320,8 +223,8 @@ Support for a growing number of locales (see ``locales.py`` for supported langua 'через 2 час(а,ов)' -Ranges & spans -============== +Ranges & Spans +~~~~~~~~~~~~~~ Get the time span of any unit: @@ -374,7 +277,7 @@ Or just iterate over a range of time: :maxdepth: 2 Factories -========= +~~~~~~~~~ Use factories to harness Arrow's module API for a custom Arrow-derived type. First, derive your type: @@ -405,7 +308,7 @@ Then get and use a factory for it: >>> 211 Tokens -====== +~~~~~~ Use the following tokens in parsing and formatting. Note that they're not the same as the tokens for `strptime(3) `_: @@ -486,30 +389,29 @@ Any token can be escaped when parsing by enclosing it within square brackets: ---------- API Guide --------- arrow.arrow -=========== +~~~~~~~~~~~ .. automodule:: arrow.arrow :members: arrow.factory -============= +~~~~~~~~~~~~~ .. automodule:: arrow.factory :members: arrow.api -========= +~~~~~~~~~ .. automodule:: arrow.api :members: arrow.locale -============ +~~~~~~~~~~~~ .. automodule:: arrow.locales :members: diff --git a/setup.py b/setup.py index 7f467c5a6..c1e2b6566 100644 --- a/setup.py +++ b/setup.py @@ -1,24 +1,18 @@ # -*- coding: utf-8 -*- import io -import re from setuptools import setup with io.open("README.rst", "r", encoding="utf-8") as f: readme = f.read() -with io.open("arrow/__init__.py", "r", encoding="utf-8") as f: - init = f.read() - - -def get_version(): - pattern = r'{}\W*=\W*"([^"]+)"'.format("__version__") - return re.findall(pattern, init)[0] - +about = {} +with io.open("arrow/_version.py", "r", encoding="utf-8") as f: + exec(f.read(), about) setup( name="arrow", - version=get_version(), + version=about["__version__"], description="Better dates and times for Python", long_description=readme, long_description_content_type="text/x-rst", @@ -50,5 +44,6 @@ def get_version(): project_urls={ "Repository": "https://github.com/crsmithdev/arrow", "Bug Reports": "https://github.com/crsmithdev/arrow/issues", + "Documentation": "https://arrow.readthedocs.io", }, ) diff --git a/tox.ini b/tox.ini index b6380ab77..8fb5ab89b 100644 --- a/tox.ini +++ b/tox.ini @@ -4,12 +4,13 @@ skip_missing_interpreters = true [testenv] deps = -rrequirements.txt -commands = - nosetests - pre-commit install +whitelist_externals = nosetests +commands = nosetests [testenv:lint] basepython = python3 skip_install = true deps = pre-commit -commands = pre-commit run --all-files --show-diff-on-failure +commands = + pre-commit install + pre-commit run --all-files --show-diff-on-failure From 81dd26ad6337da1292e85020efd12beda56e4ebc Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 8 Jul 2019 23:10:16 -0400 Subject: [PATCH 209/649] Removed _version.py encoding pragma --- .pre-commit-config.yaml | 1 + arrow/__init__.py | 1 + arrow/_version.py | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 242112843..825d4569d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,6 +7,7 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - id: fix-encoding-pragma + exclude: ^arrow/_version.py - id: check-yaml - id: debug-statements - id: requirements-txt-fixer diff --git a/arrow/__init__.py b/arrow/__init__.py index 2a6d50339..9b5fa6873 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from ._version import __version__ from .api import get, now, utcnow from .arrow import Arrow from .factory import ArrowFactory diff --git a/arrow/_version.py b/arrow/_version.py index 1a8ad57d3..745162e73 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- __version__ = "0.14.2" From 4c331c7203c4c272929a59cd0514a76f17510951 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 9 Jul 2019 00:11:55 -0400 Subject: [PATCH 210/649] Tweaked space and t divider checks and added a few more test inputs --- arrow/parser.py | 17 +++++++++++------ tests/parser_tests.py | 6 ++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index d9ac5bee3..754b2f978 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -107,21 +107,26 @@ def parse_iso(self, datetime_string): # TODO: talk to Chris about this => the below space divider checks # are not really necessary thanks to the new regex changes, but I think # it is good to include them to provide better error messages. + # my rationale is that it is better to fail early # strip leading and trailing whitespace datetime_string = datetime_string.strip() has_space_divider = " " in datetime_string + has_t_divider = "T" in datetime_string - num_space_dividers = len(datetime_string.split(" ")) - if has_space_divider and num_space_dividers != 2: + num_spaces = datetime_string.count(" ") + if (has_space_divider and num_spaces != 1) or ( + has_t_divider and num_spaces > 0 + ): + # TODO: update this message since "ISO 8601-like" may not be clear raise ParserError( - "Expected 1 space divider, but was given {}. Try passing in a format string to resolve this.".format( - num_space_dividers + "Expected an ISO 8601-like string, but was given '{}'. Try passing in a format string to resolve this.".format( + datetime_string ) ) - has_time = has_space_divider or "T" in datetime_string + has_time = has_space_divider or has_t_divider has_tz = False # TODO: add tests for all the new formats, especially basic format @@ -174,7 +179,7 @@ def parse_iso(self, datetime_string): time_string = "HH" else: # TODO: add tests for new conditional cases - raise ValueError("No valid time component provided.") + raise ParserError("No valid time component provided.") if is_basic_time_format: time_string = time_string.replace(":", "") diff --git a/tests/parser_tests.py b/tests/parser_tests.py index c1679e245..7e9766223 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -654,6 +654,12 @@ def test_parse_with_extra_words_at_start_and_end_invalid(self): ("blah2016", "YYYY"), ("blah2016blah", "YYYY"), ("blah 2016 blah", None), + ("blah 2016", None), + ("2016 blah", None), + ("blah 2016-05-16 04:05:06.789120", None), + ("2016-05-16 04:05:06.789120 blah", None), + ("blah 2016-05-16T04:05:06.789120", None), + ("2016-05-16T04:05:06.789120 blah", None), ("2016blah", "YYYY"), ("2016-05blah", "YYYY-MM"), ("2016-05-16blah", "YYYY-MM-DD"), From 08df5684674369f809f568691c034203539baed6 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 9 Jul 2019 00:34:58 -0400 Subject: [PATCH 211/649] Split up recently added parse_iso and parse unit tests and removed from_parse_iso flag --- arrow/parser.py | 8 +- tests/parser_tests.py | 203 ++++++++++++++++++++++++++---------------- 2 files changed, 129 insertions(+), 82 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 754b2f978..0677f1e5c 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -197,9 +197,9 @@ def parse_iso(self, datetime_string): formats = ["{}Z".format(f) for f in formats] # TODO: make thrown error messages less cryptic and more informative - return self._parse_multiformat(datetime_string, formats, True) + return self._parse_multiformat(datetime_string, formats) - def parse(self, datetime_string, fmt, from_parse_iso=False): + def parse(self, datetime_string, fmt): if isinstance(fmt, list): return self._parse_multiformat(datetime_string, fmt) @@ -376,13 +376,13 @@ def _build_datetime(parts): tzinfo=parts.get("tzinfo"), ) - def _parse_multiformat(self, string, formats, from_parse_iso=False): + def _parse_multiformat(self, string, formats): _datetime = None for fmt in formats: try: - _datetime = self.parse(string, fmt, from_parse_iso) + _datetime = self.parse(string, fmt) break except ParserError: pass diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 7e9766223..09522ad3c 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -22,10 +22,8 @@ def test_parse_multiformat(self): mock_datetime = self.mock() - self.expect(self.parser.parse).args("str", "fmt_a", False).raises(ParserError) - self.expect(self.parser.parse).args("str", "fmt_b", False).returns( - mock_datetime - ) + self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserError) + self.expect(self.parser.parse).args("str", "fmt_b").returns(mock_datetime) result = self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) @@ -33,8 +31,8 @@ def test_parse_multiformat(self): def test_parse_multiformat_all_fail(self): - self.expect(self.parser.parse).args("str", "fmt_a", False).raises(ParserError) - self.expect(self.parser.parse).args("str", "fmt_b", False).raises(ParserError) + self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserError) + self.expect(self.parser.parse).args("str", "fmt_b").raises(ParserError) with self.assertRaises(ParserError): self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) @@ -43,9 +41,7 @@ def test_parse_multiformat_unself_expected_fail(self): class UnselfExpectedError(Exception): pass - self.expect(self.parser.parse).args("str", "fmt_a", False).raises( - UnselfExpectedError - ) + self.expect(self.parser.parse).args("str", "fmt_a").raises(UnselfExpectedError) with self.assertRaises(UnselfExpectedError): self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) @@ -351,6 +347,103 @@ def test_parse_long_year(self): with self.assertRaises(ParserError): self.parser.parse("68096653015/01/19", "YY/M/DD") + def test_parse_with_extra_words_at_start_and_end_invalid(self): + input_format_pairs = [ + ("blah2016", "YYYY"), + ("blah2016blah", "YYYY"), + ("2016blah", "YYYY"), + ("2016-05blah", "YYYY-MM"), + ("2016-05-16blah", "YYYY-MM-DD"), + ("2016-05-16T04:05:06.789120blah", "YYYY-MM-DDThh:mm:ss.S"), + ("2016-05-16T04:05:06.789120ZblahZ", "YYYY-MM-DDThh:mm:ss.SZ"), + ("2016-05-16T04:05:06.789120Zblah", "YYYY-MM-DDThh:mm:ss.SZ"), + ("2016-05-16T04:05:06.789120blahZ", "YYYY-MM-DDThh:mm:ss.SZ"), + ] + + for pair in input_format_pairs: + with self.assertRaises(ParserError): + self.parser.parse(pair[0], pair[1]) + + def test_parse_with_extra_words_at_start_and_end_valid(self): + # Spaces surrounding the parsable date are ok because we + # allow the parsing of natural language input + self.assertEqual( + self.parser.parse("blah 2016 blah", "YYYY"), datetime(2016, 1, 1) + ) + + self.assertEqual(self.parser.parse("blah 2016", "YYYY"), datetime(2016, 1, 1)) + + self.assertEqual(self.parser.parse("2016 blah", "YYYY"), datetime(2016, 1, 1)) + + # test one additional space along with space divider + self.assertEqual( + self.parser.parse( + "blah 2016-05-16 04:05:06.789120", "YYYY-MM-DD hh:mm:ss.S" + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + self.assertEqual( + self.parser.parse( + "2016-05-16 04:05:06.789120 blah", "YYYY-MM-DD hh:mm:ss.S" + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + # test one additional space along with T divider + self.assertEqual( + self.parser.parse( + "blah 2016-05-16T04:05:06.789120", "YYYY-MM-DDThh:mm:ss.S" + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + self.assertEqual( + self.parser.parse( + "2016-05-16T04:05:06.789120 blah", "YYYY-MM-DDThh:mm:ss.S" + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + self.assertEqual( + self.parser.parse( + "Meet me at 2016-05-16T04:05:06.789120 on Tuesday", + "YYYY-MM-DDThh:mm:ss.S", + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + self.assertEqual( + self.parser.parse( + "Meet me at 2016-05-16 04:05:06.789120 on Tuesday", + "YYYY-MM-DD hh:mm:ss.S", + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + def test_parse_with_leading_and_trailing_whitespace(self): + self.assertEqual(self.parser.parse(" 2016", "YYYY"), datetime(2016, 1, 1)) + + self.assertEqual(self.parser.parse("2016 ", "YYYY"), datetime(2016, 1, 1)) + + self.assertEqual( + self.parser.parse(" 2016 ", "YYYY"), datetime(2016, 1, 1) + ) + + self.assertEqual( + self.parser.parse( + " 2016-05-16 04:05:06.789120 ", "YYYY-MM-DD hh:mm:ss.S" + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + self.assertEqual( + self.parser.parse( + " 2016-05-16T04:05:06.789120 ", "YYYY-MM-DDThh:mm:ss.S" + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + class DateTimeParserRegexTests(Chai): def setUp(self): @@ -647,96 +740,50 @@ def test_isoformat(self): self.assertEqual(self.parser.parse_iso(dt.isoformat()), dt) - def test_parse_with_extra_words_at_start_and_end_invalid(self): - # The tuple's second entry is None if the datetime string - # is valid when a format string is passed in - input_format_pairs = [ - ("blah2016", "YYYY"), - ("blah2016blah", "YYYY"), - ("blah 2016 blah", None), - ("blah 2016", None), - ("2016 blah", None), - ("blah 2016-05-16 04:05:06.789120", None), - ("2016-05-16 04:05:06.789120 blah", None), - ("blah 2016-05-16T04:05:06.789120", None), - ("2016-05-16T04:05:06.789120 blah", None), - ("2016blah", "YYYY"), - ("2016-05blah", "YYYY-MM"), - ("2016-05-16blah", "YYYY-MM-DD"), - ("2016-05-16T04:05:06.789120blah", "YYYY-MM-DDThh:mm:ss.S"), - ("2016-05-16T04:05:06.789120ZblahZ", "YYYY-MM-DDThh:mm:ss.SZ"), - ("2016-05-16T04:05:06.789120Zblah", "YYYY-MM-DDThh:mm:ss.SZ"), - ("2016-05-16T04:05:06.789120blahZ", "YYYY-MM-DDThh:mm:ss.SZ"), - ("Meet me at 2016-05-16T04:05:06.789120 on Tuesday", None), - ("Meet me at 2016-05-16 04:05:06.789120 on Tuesday", None), + def test_parse_iso_with_extra_words_at_start_and_end_invalid(self): + test_inputs = [ + "blah2016", + "blah2016blah", + "blah 2016 blah", + "blah 2016", + "2016 blah", + "blah 2016-05-16 04:05:06.789120", + "2016-05-16 04:05:06.789120 blah", + "blah 2016-05-16T04:05:06.789120", + "2016-05-16T04:05:06.789120 blah", + "2016blah", + "2016-05blah", + "2016-05-16blah", + "2016-05-16T04:05:06.789120blah", + "2016-05-16T04:05:06.789120ZblahZ", + "2016-05-16T04:05:06.789120Zblah", + "2016-05-16T04:05:06.789120blahZ", + "Meet me at 2016-05-16T04:05:06.789120 on Tuesday", + "Meet me at 2016-05-16 04:05:06.789120 on Tuesday", ] - for pair in input_format_pairs: + for ti in test_inputs: with self.assertRaises(ParserError): - self.parser.parse_iso(pair[0]) - - if pair[1] is not None: - with self.assertRaises(ParserError): - self.parser.parse(pair[0], pair[1]) - - def test_parse_with_extra_words_at_start_and_end_valid(self): - # Spaces surrounding the parsable date are ok because we - # allow the parsing of natural language input - self.assertEqual( - self.parser.parse("blah 2016 blah", "YYYY"), datetime(2016, 1, 1) - ) - - self.assertEqual( - self.parser.parse( - "Meet me at 2016-05-16T04:05:06.789120 on Tuesday", - "YYYY-MM-DDThh:mm:ss.S", - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) - - self.assertEqual( - self.parser.parse( - "Meet me at 2016-05-16 04:05:06.789120 on Tuesday", - "YYYY-MM-DD hh:mm:ss.S", - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) + self.parser.parse_iso(ti) - def test_parse_with_leading_and_trailing_whitespace(self): + def test_parse_iso_with_leading_and_trailing_whitespace(self): self.assertEqual(self.parser.parse_iso(" 2016"), datetime(2016, 1, 1)) - self.assertEqual(self.parser.parse(" 2016", "YYYY"), datetime(2016, 1, 1)) self.assertEqual(self.parser.parse_iso("2016 "), datetime(2016, 1, 1)) - self.assertEqual(self.parser.parse("2016 ", "YYYY"), datetime(2016, 1, 1)) self.assertEqual( self.parser.parse_iso(" 2016 "), datetime(2016, 1, 1) ) - self.assertEqual( - self.parser.parse(" 2016 ", "YYYY"), datetime(2016, 1, 1) - ) self.assertEqual( self.parser.parse_iso(" 2016-05-16 04:05:06.789120 "), datetime(2016, 5, 16, 4, 5, 6, 789120), ) - self.assertEqual( - self.parser.parse( - " 2016-05-16 04:05:06.789120 ", "YYYY-MM-DD hh:mm:ss.S" - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) self.assertEqual( self.parser.parse_iso(" 2016-05-16T04:05:06.789120 "), datetime(2016, 5, 16, 4, 5, 6, 789120), ) - self.assertEqual( - self.parser.parse( - " 2016-05-16T04:05:06.789120 ", "YYYY-MM-DDThh:mm:ss.S" - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) class TzinfoParserTests(Chai): From 52d0d1c3548caeec9e5eaaaee15bb15e870681be Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Fri, 12 Jul 2019 23:58:47 +0100 Subject: [PATCH 212/649] Add meridians to Spanish locale --- arrow/locales.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 5a09e5b68..99c171e7e 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -376,6 +376,8 @@ class SpanishLocale(Locale): "years": "{0} años", } + meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"} + month_names = [ "", "enero", From eb96d673d0d84c7a90611672de54f032ab32e7ae Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Sat, 13 Jul 2019 16:44:46 +0100 Subject: [PATCH 213/649] Create regression test --- tests/parser_tests.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index a212f0426..0ecd13a62 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -727,6 +727,15 @@ def test_localized_meridians_capitalized(self): parser_.parse("2013-01-01 5 DU", "YYYY-MM-DD h A"), datetime(2013, 1, 1, 17) ) + # regression check for https://github.com/crsmithdev/arrow/issues/607 + def test_es_meridians(self): + parser_ = parser.DateTimeParser("es") + + self.assertEqual( + parser_.parse("Junio 30, 2019 - 08:00 pm", "MMMM DD, YYYY - hh:mm a"), + datetime(2019, 6, 30, 20, 0), + ) + class DateTimeParserMonthOrdinalDayTests(Chai): def setUp(self): From 92e7524ca6fb898812b0c58d76b4c6e951eaecbd Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 13 Jul 2019 13:15:41 -0400 Subject: [PATCH 214/649] Python 3.8 no longer allowed to fail --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index d45f032d8..35a55a21b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,9 +18,6 @@ matrix: cache: directories: - $HOME/.cache/pre-commit - allow_failures: - - python: 3.8-dev - env: TOXENV=py38 install: pip install -U codecov tox script: tox after_success: codecov From e257dd91ef32267706bc9e1baf3319f5f61e1cb0 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 13 Jul 2019 13:46:15 -0400 Subject: [PATCH 215/649] Added footnote to indicate future DDD and DDDD token support --- docs/index.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 5e171c2bb..f359331a5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -327,9 +327,9 @@ Use the following tokens in parsing and formatting. Note that they're not the s +--------------------------------+--------------+-------------------------------------------+ | |M |1, 2, 3 ... 11, 12 | +--------------------------------+--------------+-------------------------------------------+ -|**Day of Year** |DDDD |001, 002, 003 ... 364, 365 | +|**Day of Year** |DDDD [#t5]_ |001, 002, 003 ... 364, 365 | +--------------------------------+--------------+-------------------------------------------+ -| |DDD |1, 2, 3 ... 4, 5 | +| |DDD [#t5]_ |1, 2, 3 ... 4, 5 | +--------------------------------+--------------+-------------------------------------------+ |**Day of Month** |DD |01, 02, 03 ... 30, 31 | +--------------------------------+--------------+-------------------------------------------+ @@ -374,13 +374,6 @@ Use the following tokens in parsing and formatting. Note that they're not the s |**Timestamp** |X |1381685817 | +--------------------------------+--------------+-------------------------------------------+ -.. 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 - Any token can be escaped when parsing by enclosing it within square brackets: .. code-block:: python @@ -388,6 +381,13 @@ Any token can be escaped when parsing by enclosing it within square brackets: >>> arrow.get("2018-03-09 8 h 40", "YYYY-MM-DD h [h] m") +.. 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 +.. [#t5] support for the DDD and DDDD tokens will be added in a future release API Guide --------- From 7622b8bc6ed08a6844249a55b64fe877c1f79a33 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 13 Jul 2019 16:00:35 -0400 Subject: [PATCH 216/649] Fixed bugs with basic format and multiple subsecond tokens --- arrow/parser.py | 24 +++++++++------------- docs/index.rst | 2 +- tests/parser_tests.py | 47 +++---------------------------------------- tests/util_tests.py | 4 +--- 4 files changed, 15 insertions(+), 62 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 0677f1e5c..1a12605ec 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -104,11 +104,6 @@ def __init__(self, locale="en_us", cache_size=0): # TODO: since we support more than ISO-8601, we should rename this function def parse_iso(self, datetime_string): - # TODO: talk to Chris about this => the below space divider checks - # are not really necessary thanks to the new regex changes, but I think - # it is good to include them to provide better error messages. - # my rationale is that it is better to fail early - # strip leading and trailing whitespace datetime_string = datetime_string.strip() @@ -119,7 +114,6 @@ def parse_iso(self, datetime_string): if (has_space_divider and num_spaces != 1) or ( has_t_divider and num_spaces > 0 ): - # TODO: update this message since "ISO 8601-like" may not be clear raise ParserError( "Expected an ISO 8601-like string, but was given '{}'. Try passing in a format string to resolve this.".format( datetime_string @@ -149,18 +143,19 @@ def parse_iso(self, datetime_string): ] if has_time: + # Z is ignored entirely because fromdatetime defaults to UTC in arrow.py + if datetime_string[-1] == "Z": + datetime_string = datetime_string[:-1] + if has_space_divider: date_string, time_string = datetime_string.split(" ", 1) else: date_string, time_string = datetime_string.split("T", 1) - # TODO: understand why we are not accounting for Z directly - # currently Z is ignored entirely but fromdatetime defaults to UTC, see arrow.py L196 - # '2013-02-03T04:05:06.78912Z' time_parts = re.split("[+-]", time_string, 1) colon_count = time_parts[0].count(":") - # TODO "20160504T010203Z" parses incorrectly, time part is HH only, due to Z changing len + # TODO: add test for basic format with Z "20160504T010203Z" is_basic_time_format = colon_count == 0 has_tz = len(time_parts) > 1 @@ -238,8 +233,7 @@ def _generate_pattern_re(self, fmt): # Extract the bracketed expressions to be reinserted later. escaped_fmt = re.sub(self._ESCAPE_RE, "#", escaped_fmt) - # Any number of S is the same as one. - escaped_fmt = re.sub("S+", "S", escaped_fmt) + escaped_data = re.findall(self._ESCAPE_RE, fmt) fmt_pattern = escaped_fmt @@ -283,8 +277,10 @@ def _generate_pattern_re(self, fmt): # Reference: https://stackoverflow.com/q/14232931/3820660 starting_word_boundary = r"(? Date: Sat, 13 Jul 2019 16:31:31 -0400 Subject: [PATCH 217/649] Added an extra french test --- tests/parser_tests.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 708e0ec9a..b61f6fe99 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -881,7 +881,7 @@ def test_localized_meridians_capitalized(self): parser_.parse("2013-01-01 5 DU", "YYYY-MM-DD h A"), datetime(2013, 1, 1, 17) ) - # regression check for https://github.com/crsmithdev/arrow/issues/607 + # regression test for issue #607 def test_es_meridians(self): parser_ = parser.DateTimeParser("es") @@ -890,6 +890,18 @@ def test_es_meridians(self): datetime(2019, 6, 30, 20, 0), ) + with self.assertRaises(ParserError): + parser_.parse( + "Junio 30, 2019 - 08:00 pasdfasdfm", "MMMM DD, YYYY - hh:mm a" + ) + + def test_fr_meridians(self): + parser_ = parser.DateTimeParser("fr") + + # the French locale always uses a 24 hour clock, so it does not support meridians + with self.assertRaises(ParserError): + parser_.parse("Janvier 30, 2019 - 08:00 pm", "MMMM DD, YYYY - hh:mm a") + class DateTimeParserMonthOrdinalDayTests(Chai): def setUp(self): From 188693d8084d88e726867721bb825415a5be9db4 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 13 Jul 2019 17:31:09 -0400 Subject: [PATCH 218/649] Fixed an issue with the timestamp not strictly matching --- arrow/parser.py | 3 ++- tests/parser_tests.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/arrow/parser.py b/arrow/parser.py index 1a12605ec..7711621cb 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -47,7 +47,7 @@ class DateTimeParser(object): _TWO_DIGIT_RE = re.compile(r"\d{2}") _TZ_RE = re.compile(r"[+\-]?\d{2}:?(\d{2})?|Z") _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") - _TIMESTAMP_RE = re.compile(r"\d+") + _TIMESTAMP_RE = re.compile(r"^\d+\.?\d+$") _BASE_INPUT_RE_MAP = { "YYYY": _FOUR_DIGIT_RE, @@ -124,6 +124,7 @@ def parse_iso(self, datetime_string): has_tz = False # TODO: add tests for all the new formats, especially basic format + # required date formats to test against formats = [ "YYYY-MM-DD", diff --git a/tests/parser_tests.py b/tests/parser_tests.py index b61f6fe99..0173fedef 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -132,6 +132,20 @@ def test_YY_and_YYYY_format_list(self): datetime(2019, 1, 15, 4, 5, 6, 789120, tzinfo=tz.tzutc()), ) + # regression test for issue #447 + def test_parse_timestamp_token(self): + # should not match on the "X" token + self.assertEqual( + self.parser.parse( + "15 Jul 2000", + ["MM/DD/YYYY", "YYYY-MM-DD", "X", "DD-MMMM-YYYY", "D MMM YYYY"], + ), + datetime(2000, 7, 15), + ) + + with self.assertRaises(ParserError): + self.parser.parse("15 Jul", "X") + class DateTimeParserParseTests(Chai): def setUp(self): From e480ea006630600b387f3e16680bc4a0dc1be1ca Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 13 Jul 2019 17:49:23 -0400 Subject: [PATCH 219/649] Added overflow error test to is_timestamp --- tests/util_tests.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/util_tests.py b/tests/util_tests.py index 885c016b5..9855a05a4 100644 --- a/tests/util_tests.py +++ b/tests/util_tests.py @@ -19,3 +19,11 @@ def test_is_timestamp(self): full_datetime = "2019-06-23T13:12:42" self.assertFalse(util.is_timestamp(full_datetime)) + + overflow_timestamp_float = 99999999999999999999999999.99999999999999999999999999 + with self.assertRaises((OverflowError, ValueError)): + util.is_timestamp(overflow_timestamp_float) + + overflow_timestamp_int = int(overflow_timestamp_float) + with self.assertRaises((OverflowError, ValueError)): + util.is_timestamp(overflow_timestamp_int) From 3f1a3c951458381bc73343ac2cd7132067dd8bf3 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 13 Jul 2019 17:52:27 -0400 Subject: [PATCH 220/649] Attempt at fixing linting --- .pre-commit-config.yaml | 2 +- .travis.yml | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 825d4569d..9ef036661 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: v1.20.1 + rev: v1.21.0 hooks: - id: pyupgrade - repo: https://github.com/pre-commit/pygrep-hooks diff --git a/.travis.yml b/.travis.yml index 35a55a21b..1bca8a256 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,9 +15,6 @@ matrix: - name: "Linting" python: 3.7 env: TOXENV=lint - cache: - directories: - - $HOME/.cache/pre-commit install: pip install -U codecov tox script: tox after_success: codecov From 17447dbcee3da0976671331d92a18636e226ef87 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Sun, 14 Jul 2019 16:13:52 +0100 Subject: [PATCH 221/649] Add warnings for .get() parsing changes in 0.15.0 --- arrow/factory.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/arrow/factory.py b/arrow/factory.py index 8591443a4..437fe4496 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -9,6 +9,7 @@ from __future__ import absolute_import import calendar +import warnings from datetime import date, datetime, tzinfo from time import struct_time @@ -19,6 +20,21 @@ from arrow.util import is_timestamp, isstr +class ArrowParseWarning(DeprecationWarning): + """Raised when arrow.get() is passed a string with no formats and matches incorrectly + on one of the default formats. + + e.g. + arrow.get('blabla2016') -> + arrow.get('13/4/2045') -> + + In version 0.15.0 this warning will become a ParserError. + """ + + +warnings.simplefilter("always", ArrowParseWarning) + + class ArrowFactory(object): """ A factory for generating :class:`Arrow ` objects. @@ -172,6 +188,11 @@ def get(self, *args, **kwargs): # (str) -> parse. elif isstr(arg): + warnings.warn( + "The .get() parsing method without a format string will parse more strictly in version 0.15.0." + "See https://github.com/crsmithdev/arrow/issues/612 for more details.", + ArrowParseWarning, + ) dt = parser.DateTimeParser(locale).parse_iso(arg) return self.type.fromdatetime(dt, tz) @@ -214,6 +235,11 @@ def get(self, *args, **kwargs): # (str, format) -> parse. elif isstr(arg_1) and (isstr(arg_2) or isinstance(arg_2, list)): + warnings.warn( + "The .get() parsing method with a format string will parse more strictly in version 0.15.0." + "See https://github.com/crsmithdev/arrow/issues/612 for more details.", + ArrowParseWarning, + ) dt = parser.DateTimeParser(locale).parse(args[0], args[1]) return self.type.fromdatetime(dt, tzinfo=tz) From 87f63fb632130ea6bcba2121d6a46c1aac4bdc04 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 14 Jul 2019 13:08:14 -0400 Subject: [PATCH 222/649] Added support for DDD and DDDD tokens in arrow.get() --- arrow/parser.py | 60 +++++++++++++++++++++++++++++++++++-------------- docs/index.rst | 5 ++--- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 7711621cb..a45e620b7 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -2,7 +2,6 @@ from __future__ import absolute_import, unicode_literals import re -import warnings from datetime import datetime from dateutil import tz @@ -19,21 +18,6 @@ class ParserError(RuntimeError): pass -class GetParseWarning(DeprecationWarning): - """Raised when arrow.get() is passed a string with no formats and matches incorrectly - on one of the default formats. - - e.g. - arrow.get('blabla2016') -> - arrow.get('13/4/2045') -> - - In version 0.15.0 this warning will become a ParserError. - """ - - -warnings.simplefilter("default", GetParseWarning) - - class DateTimeParser(object): _FORMAT_RE = re.compile( @@ -43,6 +27,8 @@ class DateTimeParser(object): _ONE_OR_MORE_DIGIT_RE = re.compile(r"\d+") _ONE_OR_TWO_DIGIT_RE = re.compile(r"\d{1,2}") + _ONE_OR_TWO_OR_THREE_DIGIT_RE = re.compile(r"\d{1,3}") + _THREE_DIGIT_RE = re.compile(r"\d{3}") _FOUR_DIGIT_RE = re.compile(r"\d{4}") _TWO_DIGIT_RE = re.compile(r"\d{2}") _TZ_RE = re.compile(r"[+\-]?\d{2}:?(\d{2})?|Z") @@ -54,6 +40,8 @@ class DateTimeParser(object): "YY": _TWO_DIGIT_RE, "MM": _TWO_DIGIT_RE, "M": _ONE_OR_TWO_DIGIT_RE, + "DDDD": _THREE_DIGIT_RE, + "DDD": _ONE_OR_TWO_OR_THREE_DIGIT_RE, "DD": _TWO_DIGIT_RE, "D": _ONE_OR_TWO_DIGIT_RE, "HH": _TWO_DIGIT_RE, @@ -125,7 +113,7 @@ def parse_iso(self, datetime_string): # TODO: add tests for all the new formats, especially basic format - # required date formats to test against + # date formats (ISO-8601 and others) to test against formats = [ "YYYY-MM-DD", "YYYY-M-DD", @@ -137,6 +125,8 @@ def parse_iso(self, datetime_string): "YYYY.M.DD", "YYYY.M.D", "YYYYMMDD", + "YYYY-DDDD", + "YYYYDDDD", "YYYY-MM", "YYYY/MM", "YYYY.MM", @@ -301,6 +291,9 @@ def _parse_token(self, token, value, parts): elif token in ["MM", "M"]: parts["month"] = int(value) + elif token in ["DDDD", "DDD"]: + parts["day_of_year"] = int(value) + elif token in ["DD", "D"]: parts["day"] = int(value) @@ -354,6 +347,39 @@ def _build_datetime(parts): tz_utc = tz.tzutc() return datetime.fromtimestamp(timestamp, tz=tz_utc) + # TODO: add tests for this! + day_of_year = parts.get("day_of_year") + + if day_of_year: + year = parts.get("year") + month = parts.get("month") + if year is None: + raise ParserError( + "Year component is required with the DDD and DDDD tokens" + ) + + if month is not None: + raise ParserError( + "Month component is not allowed with the DDD and DDDD tokens" + ) + + date_string = "{}-{}".format(year, day_of_year) + try: + dt = datetime.strptime(date_string, "%Y-%j") + except ValueError: + raise ParserError( + "Expected a valid day of year, but received '{}'".format( + day_of_year + ) + ) + + # TODO: write test for 2015-366 + # datetime.strptime("2015-366", "%Y-%j") + # Changes year: datetime.datetime(2016, 1, 1, 0, 0) + parts["year"] = dt.year + parts["month"] = dt.month + parts["day"] = dt.day + am_pm = parts.get("am_pm") hour = parts.get("hour", 0) diff --git a/docs/index.rst b/docs/index.rst index a0a7775d2..7decad748 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -327,9 +327,9 @@ Use the following tokens in parsing and formatting. Note that they're not the s +--------------------------------+--------------+-------------------------------------------+ | |M |1, 2, 3 ... 11, 12 | +--------------------------------+--------------+-------------------------------------------+ -|**Day of Year** |DDDD [#t5]_ |001, 002, 003 ... 364, 365 | +|**Day of Year** |DDDD |001, 002, 003 ... 364, 365 | +--------------------------------+--------------+-------------------------------------------+ -| |DDD [#t5]_ |1, 2, 3 ... 4, 5 | +| |DDD |1, 2, 3 ... 364, 365 | +--------------------------------+--------------+-------------------------------------------+ |**Day of Month** |DD |01, 02, 03 ... 30, 31 | +--------------------------------+--------------+-------------------------------------------+ @@ -387,7 +387,6 @@ Any token can be escaped when parsing by enclosing it within square brackets: .. [#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 -.. [#t5] support for the DDD and DDDD tokens will be added in a future release API Guide --------- From a6995bafdf87b573c8230181fec31c077b19ee87 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 14 Jul 2019 14:54:15 -0400 Subject: [PATCH 223/649] Cleaned up timestamp docs --- docs/index.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 7decad748..fd1754ea2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,22 +26,16 @@ Get 'now' easily: >>> arrow.now('US/Pacific') -Create from timestamps (ints or floats, or strings that convert to a float): +Create from timestamps (:code:`int` or :code:`float`): .. code-block:: python >>> arrow.get(1367900664) - >>> arrow.get('1367900664') - - >>> arrow.get(1367900664.152325) - >>> arrow.get('1367900664.152325') - - Use a naive or timezone-aware datetime, or flexibly specify a timezone: .. code-block:: python From 074dddf566093d6e10c97c00154aec3a4fd464ef Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Sun, 14 Jul 2019 20:17:54 +0100 Subject: [PATCH 224/649] Simplify conditional and create extra tests --- arrow/factory.py | 4 +--- tests/factory_tests.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/arrow/factory.py b/arrow/factory.py index fa6f2fc0b..113b2c148 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -141,9 +141,7 @@ def get(self, *args, **kwargs): if len(kwargs) > 1: arg_count = 3 if len(kwargs) == 1: - if isinstance(tz, tzinfo): - pass - else: + if not isinstance(tz, tzinfo): arg_count = 3 # () -> now, @ utc. diff --git a/tests/factory_tests.py b/tests/factory_tests.py index e7e4c8870..416530e69 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -221,6 +221,21 @@ def test_three_args(self): self.factory.get(2013, 1, 1), datetime(2013, 1, 1, tzinfo=tz.tzutc()) ) + def test_full_kwargs(self): + + self.assertEqual( + 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): self.assertEqual( @@ -233,6 +248,19 @@ def test_insufficient_kwargs(self): with self.assertRaises(TypeError): self.factory.get(year=2016) + with self.assertRaises(TypeError): + self.factory.get(year=2016, month=7) + + def test_locale_kwarg_only(self): + + with self.assertRaises(TypeError): + self.factory.get(locale="ja") + + def test_locale_with_tzinfo(self): + + with self.assertRaises(TypeError): + self.factory.get(locale="ja", tzinfo=tz.gettz("Asia/Tokyo")) + class UtcNowTests(Chai): def setUp(self): From 3f65022424bc19cdc54ce3d58982cac6ad3e1eae Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 14 Jul 2019 17:01:01 -0500 Subject: [PATCH 225/649] Added escaping to docs (#614) * Added escaping to docs * Locked dependency versions --- docs/index.rst | 54 ++++++++++++++++++++++++++++++++++++++++-------- requirements.txt | 4 ++-- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index f359331a5..1f9cd1a47 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -307,8 +307,8 @@ Then get and use a factory for it: >>> custom.days_till_xmas() >>> 211 -Tokens -~~~~~~ +Supported Tokens +~~~~~~~~~~~~~~~~ Use the following tokens in parsing and formatting. Note that they're not the same as the tokens for `strptime(3) `_: @@ -374,13 +374,6 @@ Use the following tokens in parsing and formatting. Note that they're not the s |**Timestamp** |X |1381685817 | +--------------------------------+--------------+-------------------------------------------+ -Any token can be escaped when parsing by enclosing it within square brackets: - -.. code-block:: python - - >>> arrow.get("2018-03-09 8 h 40", "YYYY-MM-DD h [h] m") - - .. rubric:: Footnotes .. [#t1] localization support for parsing and formatting @@ -389,6 +382,49 @@ Any token can be escaped when parsing by enclosing it within square brackets: .. [#t4] timezone names from `tz database `_ provided via dateutil package .. [#t5] support for the DDD and DDDD tokens will be added in a future release +Escaping Formats +~~~~~~~~~~~~~~~~ + +Tokens, phrases, and regular expressions in a format string can be escaped when parsing 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" + >>> arrow.get("2018-03-09 8 h 40", fmt) + + + >>> fmt = "YYYY-MM-DD h [hello] m" + >>> arrow.get("2018-03-09 8 hello 40", fmt) + + + >>> fmt = "YYYY-MM-DD h [hello world] m" + >>> arrow.get("2018-03-09 8 hello world 40", fmt) + + +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) + + API Guide --------- diff --git a/requirements.txt b/requirements.txt index d5ca714e7..ac54277ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,9 +2,9 @@ backports.functools_lru_cache==1.5.0 chai==1.1.2 nose==1.3.7 nose-cov==1.6 -pre-commit==1.16.1 +pre-commit==1.17.0 python-dateutil==2.8.0 pytz==2019.1 simplejson==3.16.0 sphinx==1.8.5; python_version == '2.7' -sphinx==2.0.1; python_version >= '3.5' +sphinx==2.1.2; python_version >= '3.5' From 3edbafe42f98b052164b17b4af1ef486a81ad4c2 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Sun, 28 Jul 2019 14:38:05 +0100 Subject: [PATCH 226/649] Update HISTORY.md with 0.14.3 --- HISTORY.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 221df67a6..35fdface4 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,13 @@ ## History +### 0.14.3 + +- [NEW] Added full support for Python 3.8. +- [CHANGE] Added warnings for upcoming factory.get() parsing changes in 0.15.0. Please see https://github.com/crsmithdev/arrow/issues/612 for full details. +- [FIX] Extensive refactor and update of documentation. +- [FIX] factory.get() can now construct from kwargs. +- [FIX] Added meridians to Spanish Locale. + ### 0.14.2 - [CHANGE] Travis CI builds now use tox to lint and run tests. From 75a9318f5f1d66baadf67f6bcfb347b2403ef33b Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 28 Jul 2019 10:31:03 -0400 Subject: [PATCH 227/649] Bump version and prepare for release --- Makefile | 9 ++++++++- arrow/_version.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 2b534ea60..427a68e00 100644 --- a/Makefile +++ b/Makefile @@ -29,5 +29,12 @@ docs: clean: rm -rf venv .tox ./**/__pycache__ - rm -rf dist build .egg arrow.egg-info + rm -rf dist build .egg .eggs arrow.egg-info rm -f ./**/*.pyc .coverage + +publish: + rm -rf dist build .egg .eggs arrow.egg-info + pip3 install -U setuptools twine wheel + python3 setup.py sdist bdist_wheel + twine upload dist/* + rm -rf dist build .egg .eggs arrow.egg-info diff --git a/arrow/_version.py b/arrow/_version.py index 745162e73..23f00709c 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.14.2" +__version__ = "0.14.3" From b5c626ee1cdac2b42fbb98d47608b885210c00a6 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 28 Jul 2019 10:33:50 -0400 Subject: [PATCH 228/649] Spaces vs tabs please --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 427a68e00..af4345e8d 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ clean: rm -f ./**/*.pyc .coverage publish: - rm -rf dist build .egg .eggs arrow.egg-info + rm -rf dist build .egg .eggs arrow.egg-info pip3 install -U setuptools twine wheel python3 setup.py sdist bdist_wheel twine upload dist/* From d4bcb73530e7cade1a098062b1f5d1eac41434e3 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 28 Jul 2019 11:51:41 -0400 Subject: [PATCH 229/649] Added tests for new timestring changes --- arrow/parser.py | 6 ++++-- tests/parser_tests.py | 48 +++++++++++++++---------------------------- 2 files changed, 21 insertions(+), 33 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index a45e620b7..912d56bca 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -34,6 +34,7 @@ class DateTimeParser(object): _TZ_RE = re.compile(r"[+\-]?\d{2}:?(\d{2})?|Z") _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") _TIMESTAMP_RE = re.compile(r"^\d+\.?\d+$") + # TODO: test timestamp thoroughly _BASE_INPUT_RE_MAP = { "YYYY": _FOUR_DIGIT_RE, @@ -164,8 +165,9 @@ def parse_iso(self, datetime_string): elif has_hours: time_string = "HH" else: - # TODO: add tests for new conditional cases - raise ParserError("No valid time component provided.") + raise ParserError( + "Invalid time component provided. Please specify a format or provide a time in the form 'HH:mm:ss.S', 'HH:mm:ss', 'HH:mm', or 'HH'." + ) if is_basic_time_format: time_string = time_string.replace(":", "") diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 0173fedef..f1f9b9607 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -294,7 +294,6 @@ def test_parse_subsecond_rounding(self): # round down string = "2013-01-01 12:30:45.98765432" self.assertEqual(self.parser.parse(string, format), self.expected) - # import pudb; pudb.set_trace() self.assertEqual(self.parser.parse_iso(string), self.expected) # round half-up @@ -567,6 +566,23 @@ def test_YYYY_MM_DD_HH_mm(self): self.parser.parse_iso("2013-02-03 04:05"), datetime(2013, 2, 3, 4, 5) ) + def test_YYYY_MM_DD_HH(self): + + self.assertEqual( + self.parser.parse_iso("2013-02-03 04"), datetime(2013, 2, 3, 4) + ) + + def test_invalid_time(self): + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03 044") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03 04:05:06.") + def test_YYYY_MM_DD_HH_mm_ssZ(self): self.assertEqual( @@ -657,36 +673,6 @@ def test_YYYY_MM_DDTHH_mm_ss_SZ(self): datetime(2013, 2, 3, 4, 5, 6, 789120), ) - # def test_bad_get_parsing(self): - # # fixes for loose get parsing - # - # with self.assertWarns(GetParseWarning): - # self.parser.parse_iso("blabla2016") - # - # with self.assertWarns(GetParseWarning): - # self.parser.parse_iso("2016blabla") - # - # with self.assertWarns(GetParseWarning): - # self.parser.parse_iso("10/4/2045") - # - # with self.assertWarns(GetParseWarning): - # self.parser.parse_iso("2016-05T04:05:06.78912blahZ") - # - # with self.assertWarns(GetParseWarning): - # self.parser.parse_iso("2016-05T04:05:06.78912Zblah") - # - # with self.assertWarns(GetParseWarning): - # self.parser.parse("15/01/2019", ["D/M/YY", "D/M/YYYY"]) - # - # with self.assertWarns(GetParseWarning): - # self.parser.parse("05/02/2017", ["YYYY", "MM/DD/YYYY"]) - # - # with self.assertWarns(GetParseWarning): - # self.parser.parse("1919/05/23", ["YY/M/D", "YYYY/M/D"]) - # - # with self.assertWarns(GetParseWarning): - # self.parser.parse("2017/05/22", ["YYYY", "YYYY/MM/DD"]) - def test_gnu_date(self): """ regression tests for parsing output from GNU date(1) From 1011d9bdddbe430ec5751df0a20ee1154dbaac58 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 29 Jul 2019 10:07:26 -0700 Subject: [PATCH 230/649] Fix regression when tzinfo kwarg is a string --- arrow/factory.py | 2 +- tests/factory_tests.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/arrow/factory.py b/arrow/factory.py index ba705d3d1..fc062084f 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -157,7 +157,7 @@ def get(self, *args, **kwargs): if len(kwargs) > 1: arg_count = 3 if len(kwargs) == 1: - if not isinstance(tz, tzinfo): + if not tz: arg_count = 3 # () -> now, @ utc. diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 416530e69..635f0d0d4 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -243,6 +243,12 @@ def test_three_kwargs(self): datetime(2016, 7, 14, 0, 0, tzinfo=tz.tzutc()), ) + def test_tzinfo_string_kwargs(self): + result = self.factory.get("2019072807", "YYYYMMDDHH", tzinfo="UTC") + self.assertEqual( + result._datetime, datetime(2019, 7, 28, 7, 0, 0, 0, tzinfo=tz.tzutc()) + ) + def test_insufficient_kwargs(self): with self.assertRaises(TypeError): From 4d655756062016c6bb415e30839ef7568b28e090 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 30 Jul 2019 13:29:29 -0400 Subject: [PATCH 231/649] Move backports from extra_requires to install_requires (#620) * Add backports to install_requires * Added keywords * Lock backports version >= 1.2.1 --- setup.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index c1e2b6566..dec463a4e 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name="arrow", version=about["__version__"], - description="Better dates and times for Python", + description="Better dates & times for Python", long_description=readme, long_description_content_type="text/x-rst", url="https://arrow.readthedocs.io/en/latest/", @@ -23,8 +23,10 @@ packages=["arrow"], zip_safe=False, python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", - install_requires=["python-dateutil"], - extras_require={":python_version=='2.7'": ["backports.functools_lru_cache>=1.2.1"]}, + install_requires=[ + "python-dateutil", + "backports.functools_lru_cache>=1.2.1;python_version=='2.7'", + ], test_suite="tests", tests_require=["chai"], classifiers=[ @@ -40,7 +42,7 @@ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", ], - keywords="arrow date time datetime", + keywords="arrow date time datetime timestamp timezone", project_urls={ "Repository": "https://github.com/crsmithdev/arrow", "Bug Reports": "https://github.com/crsmithdev/arrow/issues", From a4893b5478882abcc5dc8498fc943c0cb1b41813 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 30 Jul 2019 13:32:37 -0400 Subject: [PATCH 232/649] Tweaked factory kwargs handling (#622) --- arrow/factory.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/arrow/factory.py b/arrow/factory.py index fc062084f..c4c7291d8 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -153,12 +153,13 @@ def get(self, *args, **kwargs): locale = kwargs.get("locale", "en_us") tz = kwargs.get("tzinfo", None) - # if kwargs given, send to constructor unless only tzinfo provided. + # if kwargs given, send to constructor unless only tzinfo provided if len(kwargs) > 1: arg_count = 3 - if len(kwargs) == 1: - if not tz: - arg_count = 3 + + # tzinfo kwarg is not provided + if len(kwargs) == 1 and tz is None: + arg_count = 3 # () -> now, @ utc. if arg_count == 0: From 6c8fbbad71f1d9a69b803de2a1950e04cef2fa42 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Tue, 30 Jul 2019 22:47:44 +0100 Subject: [PATCH 233/649] Add tests for DDD and DDDD tokens --- tests/parser_tests.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index f1f9b9607..6f4cf0e0f 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -416,6 +416,28 @@ def test_parse_with_leading_and_trailing_whitespace(self): datetime(2016, 5, 16, 4, 5, 6, 789120), ) + def test_parse_YYYY_DDDD(self): + self.assertEqual( + self.parser.parse("1998-136", "YYYY-DDDD"), datetime(1998, 5, 16) + ) + + self.assertEqual( + self.parser.parse("1998-006", "YYYY-DDDD"), datetime(1998, 1, 6) + ) + + with self.assertRaises(ParserError): + self.parser.parse("1998-456", "YYYY-DDDD") + + def test_parse_YYYY_DDD(self): + self.assertEqual(self.parser.parse("1998-6", "YYYY-DDD"), datetime(1998, 1, 6)) + + self.assertEqual( + self.parser.parse("1998-136", "YYYY-DDD"), datetime(1998, 5, 16) + ) + + with self.assertRaises(ParserError): + self.parser.parse("1998-756", "YYYY-DDD") + class DateTimeParserRegexTests(Chai): def setUp(self): @@ -511,6 +533,14 @@ def test_YYYY(self): self.assertEqual(self.parser.parse_iso("2013"), datetime(2013, 1, 1)) + def test_parse_YYYY_DDDD(self): + self.assertEqual(self.parser.parse_iso("1998-136"), datetime(1998, 5, 16)) + + self.assertEqual(self.parser.parse_iso("1998-006"), datetime(1998, 1, 6)) + + with self.assertRaises(ParserError): + self.parser.parse_iso("1998-456") + def test_YYYY_MM(self): for separator in DateTimeParser.SEPARATORS: From b9d1171cc0bbae3cdeb445c6743c6b065c191504 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 30 Jul 2019 18:09:31 -0400 Subject: [PATCH 234/649] Release notes and increment version (#623) --- HISTORY.md | 5 +++++ arrow/_version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 35fdface4..c430308dc 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ ## History +### 0.14.4 + +- [FIX] Fixed a regression in 0.14.3 that prevented a tzinfo argument of type string to be passed to the `get()` function. Functionality such as `arrow.get("2019072807", "YYYYMMDDHH", tzinfo="UTC")` should work as normal again. +- [CHANGE] Moved `backports.functools_lru_cache` dependency from `extra_requires` to `install_requires` for `Python 2.7` installs to fix [#495](https://github.com/crsmithdev/arrow/issues/495). + ### 0.14.3 - [NEW] Added full support for Python 3.8. diff --git a/arrow/_version.py b/arrow/_version.py index 23f00709c..224f1fb74 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.14.3" +__version__ = "0.14.4" From 976b6ea3554185d20e084a1d5b2e3c7294946e63 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 1 Aug 2019 05:55:44 -0400 Subject: [PATCH 235/649] Removed deprecated replace shift functionality (#624) * Removed deprecated replace shift functionality * add back quarters bug test --- arrow/arrow.py | 17 ------- tests/arrow_tests.py | 111 ++++--------------------------------------- 2 files changed, 8 insertions(+), 120 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 19bbbe0b4..08b9b571d 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -9,7 +9,6 @@ import calendar import sys -import warnings from datetime import datetime, timedelta, tzinfo from math import trunc @@ -580,33 +579,17 @@ def replace(self, **kwargs): """ absolute_kwargs = {} - relative_kwargs = {} # TODO: DEPRECATED; remove in next release for key, value in kwargs.items(): if key in self._ATTRS: absolute_kwargs[key] = value - elif key in self._ATTRS_PLURAL or key in ["weeks", "quarters"]: - # TODO: DEPRECATED - warnings.warn( - "replace() with plural property to shift value " - "is deprecated, use shift() instead", - DeprecationWarning, - ) - relative_kwargs[key] = value elif key in ["week", "quarter"]: raise AttributeError("setting absolute {} is not supported".format(key)) elif key != "tzinfo": raise AttributeError('unknown attribute: "{}"'.format(key)) - # core datetime does not support quarters, translate to months. - relative_kwargs.setdefault("months", 0) - relative_kwargs["months"] += ( - relative_kwargs.pop("quarters", 0) * self._MONTHS_PER_QUARTER - ) - current = self._datetime.replace(**absolute_kwargs) - current += relativedelta(**relative_kwargs) # TODO: DEPRECATED tzinfo = kwargs.get("tzinfo") diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 825db674c..cd3578655 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -228,28 +228,6 @@ def test_ne(self): self.assertFalse(self.arrow != self.arrow.datetime) self.assertTrue(self.arrow != "abc") - def test_deprecated_replace(self): - - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") - # Trigger a warning. - self.arrow.replace(weeks=1) - # Verify some things - assert len(w) == 1 - assert issubclass(w[-1].category, DeprecationWarning) - assert "deprecated" in str(w[-1].message) - - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") - # Trigger a warning. - self.arrow.replace(hours=1) - # Verify some things - assert len(w) == 1 - assert issubclass(w[-1].category, DeprecationWarning) - assert "deprecated" in str(w[-1].message) - def test_gt(self): arrow_cmp = self.arrow.shift(minutes=1) @@ -499,79 +477,6 @@ def test_replace(self): self.assertEqual(arw.replace(minute=1), arrow.Arrow(2013, 5, 5, 12, 1, 45)) self.assertEqual(arw.replace(second=1), arrow.Arrow(2013, 5, 5, 12, 30, 1)) - def test_replace_shift(self): - - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) - - # This is all scheduled for deprecation - self.assertEqual(arw.replace(years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) - self.assertEqual(arw.replace(quarters=1), arrow.Arrow(2013, 8, 5, 12, 30, 45)) - self.assertEqual( - arw.replace(quarters=1, months=1), arrow.Arrow(2013, 9, 5, 12, 30, 45) - ) - self.assertEqual(arw.replace(months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) - self.assertEqual(arw.replace(weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) - self.assertEqual(arw.replace(days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - self.assertEqual(arw.replace(hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) - self.assertEqual(arw.replace(minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) - self.assertEqual(arw.replace(seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) - self.assertEqual( - arw.replace(microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1) - ) - - def test_replace_shift_negative(self): - - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) - - # This is all scheduled for deprecation - self.assertEqual(arw.replace(years=-1), arrow.Arrow(2012, 5, 5, 12, 30, 45)) - self.assertEqual(arw.replace(quarters=-1), arrow.Arrow(2013, 2, 5, 12, 30, 45)) - self.assertEqual( - arw.replace(quarters=-1, months=-1), arrow.Arrow(2013, 1, 5, 12, 30, 45) - ) - self.assertEqual(arw.replace(months=-1), arrow.Arrow(2013, 4, 5, 12, 30, 45)) - self.assertEqual(arw.replace(weeks=-1), arrow.Arrow(2013, 4, 28, 12, 30, 45)) - self.assertEqual(arw.replace(days=-1), arrow.Arrow(2013, 5, 4, 12, 30, 45)) - self.assertEqual(arw.replace(hours=-1), arrow.Arrow(2013, 5, 5, 11, 30, 45)) - self.assertEqual(arw.replace(minutes=-1), arrow.Arrow(2013, 5, 5, 12, 29, 45)) - self.assertEqual(arw.replace(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) - self.assertEqual( - arw.replace(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999) - ) - - def test_replace_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. - # Recall that the keyword argument dict, like all dicts, is unordered, so only certain - # combinations of arguments would exhibit this. - self.assertEqual( - arw.replace(quarters=0, years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45) - ) - self.assertEqual( - arw.replace(quarters=0, months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45) - ) - self.assertEqual( - arw.replace(quarters=0, weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45) - ) - self.assertEqual( - arw.replace(quarters=0, days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45) - ) - self.assertEqual( - arw.replace(quarters=0, hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45) - ) - self.assertEqual( - arw.replace(quarters=0, minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45) - ) - self.assertEqual( - arw.replace(quarters=0, seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46) - ) - self.assertEqual( - arw.replace(quarters=0, microseconds=1), - arrow.Arrow(2013, 5, 5, 12, 30, 45, 1), - ) - def test_replace_tzinfo(self): arw = arrow.Arrow.utcnow().to("US/Eastern") @@ -717,28 +622,28 @@ def test_shift_quarters_bug(self): # Recall that the keyword argument dict, like all dicts, is unordered, so only certain # combinations of arguments would exhibit this. self.assertEqual( - arw.replace(quarters=0, years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45) + arw.shift(quarters=0, years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45) ) self.assertEqual( - arw.replace(quarters=0, months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45) + arw.shift(quarters=0, months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45) ) self.assertEqual( - arw.replace(quarters=0, weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45) + arw.shift(quarters=0, weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45) ) self.assertEqual( - arw.replace(quarters=0, days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45) + arw.shift(quarters=0, days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45) ) self.assertEqual( - arw.replace(quarters=0, hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45) + arw.shift(quarters=0, hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45) ) self.assertEqual( - arw.replace(quarters=0, minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45) + arw.shift(quarters=0, minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45) ) self.assertEqual( - arw.replace(quarters=0, seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46) + arw.shift(quarters=0, seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46) ) self.assertEqual( - arw.replace(quarters=0, microseconds=1), + arw.shift(quarters=0, microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1), ) From be87f39a9f82986d33d7fcfbed1a22e3e2617fe1 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Thu, 1 Aug 2019 20:39:29 +0100 Subject: [PATCH 236/649] Further test cases for DDD and DDDD tokens --- arrow/parser.py | 1 + tests/parser_tests.py | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/arrow/parser.py b/arrow/parser.py index fc7eff61e..c412d6dfa 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -377,6 +377,7 @@ def _build_datetime(parts): ) # TODO: write test for 2015-366 + # TODO: should we throw an error or mimic datetime? # datetime.strptime("2015-366", "%Y-%j") # Changes year: datetime.datetime(2016, 1, 1, 0, 0) parts["year"] = dt.year diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 6f4cf0e0f..f4382c45c 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -438,6 +438,19 @@ def test_parse_YYYY_DDD(self): with self.assertRaises(ParserError): self.parser.parse("1998-756", "YYYY-DDD") + # month cannot be passed with DDD and DDDD tokens + def test_parse_YYYY_MM_DDDD(self): + with self.assertRaises(ParserError): + self.parser.parse("2015-01-009", "YYYY-MM-DDDD") + + def test_parse_DDD_only(self): + with self.assertRaises(ParserError): + self.parser.parse("5", "DDD") + + def test_parse_DDDD_only(self): + with self.assertRaises(ParserError): + self.parser.parse("145", "DDDD") + class DateTimeParserRegexTests(Chai): def setUp(self): @@ -533,7 +546,7 @@ def test_YYYY(self): self.assertEqual(self.parser.parse_iso("2013"), datetime(2013, 1, 1)) - def test_parse_YYYY_DDDD(self): + def test_YYYY_DDDD(self): self.assertEqual(self.parser.parse_iso("1998-136"), datetime(1998, 5, 16)) self.assertEqual(self.parser.parse_iso("1998-006"), datetime(1998, 1, 6)) @@ -541,6 +554,17 @@ def test_parse_YYYY_DDDD(self): with self.assertRaises(ParserError): self.parser.parse_iso("1998-456") + def test_YYYY_DDDD_HH_mm_ssZ(self): + + self.assertEqual( + self.parser.parse_iso("2013-036 04:05:06+01:00"), + datetime(2013, 2, 5, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600)), + ) + + def test_YYYY_MM_DDDD(self): + with self.assertRaises(ParserError): + self.parser.parse_iso("2014-05-125") + def test_YYYY_MM(self): for separator in DateTimeParser.SEPARATORS: From f02c8d73ad8253564787cea2e670751f387350ae Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Fri, 2 Aug 2019 22:12:07 +0100 Subject: [PATCH 237/649] Add test cases for iso8601 basic format Correct error in parse_iso when handling basic format timezones. --- arrow/parser.py | 12 ++++++------ tests/parser_tests.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index c412d6dfa..a710b2407 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -93,6 +93,7 @@ def __init__(self, locale="en_us", cache_size=0): ) # TODO: since we support more than ISO-8601, we should rename this function + # IDEA: break into multiple functions def parse_iso(self, datetime_string): # strip leading and trailing whitespace datetime_string = datetime_string.strip() @@ -114,7 +115,7 @@ def parse_iso(self, datetime_string): has_tz = False # TODO: add tests for all the new formats, especially basic format - + # IDEA: should YYYY MM DD style be accepted here? # date formats (ISO-8601 and others) to test against formats = [ "YYYY-MM-DD", @@ -148,13 +149,12 @@ def parse_iso(self, datetime_string): time_parts = re.split("[+-]", time_string, 1) colon_count = time_parts[0].count(":") - # TODO: add test for basic format with Z "20160504T010203Z" is_basic_time_format = colon_count == 0 has_tz = len(time_parts) > 1 - has_hours = len(time_string) == 2 - has_minutes = colon_count == 1 or len(time_string) == 4 - has_seconds = colon_count == 2 or len(time_string) == 6 + has_hours = len(time_parts[0]) == 2 + has_minutes = colon_count == 1 or len(time_parts[0]) == 4 + has_seconds = colon_count == 2 or len(time_parts[0]) == 6 has_subseconds = re.search("[.,]", time_parts[0]) if has_subseconds: @@ -178,7 +178,7 @@ def parse_iso(self, datetime_string): else: formats = ["{}T{}".format(f, time_string) for f in formats] - # TODO: reduce set of date formats for basic? + # TODO: reduce set of date formats for basic? test earlier? if has_time and has_tz: # Add "Z" to format strings to indicate to _parse_tokens diff --git a/tests/parser_tests.py b/tests/parser_tests.py index f4382c45c..5dc261c5f 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -246,9 +246,9 @@ def test_parse_tz_zz(self): def test_parse_tz_name_zzz(self): for tz_name in ( # best solution would be to test on every available tz name from - # the tz database but it is actualy tricky to retrieve them from + # the tz database but it is actually tricky to retrieve them from # dateutil so here is short list that should match all - # naming patterns/conventions in used tz databaze + # naming patterns/conventions in used tz database "Africa/Tripoli", "America/Port_of_Spain", "Australia/LHI", @@ -798,6 +798,35 @@ def test_parse_iso_with_leading_and_trailing_whitespace(self): datetime(2016, 5, 16, 4, 5, 6, 789120), ) + def test_iso8601_basic_format(self): + self.assertEqual(self.parser.parse_iso("20180517"), datetime(2018, 5, 17)) + + self.assertEqual( + self.parser.parse_iso("20180517T10"), datetime(2018, 5, 17, 10) + ) + + self.assertEqual( + self.parser.parse_iso("20180517T105513.84"), + datetime(2018, 5, 17, 10, 55, 13, 840000), + ) + + self.assertEqual( + self.parser.parse_iso("20180517T105513Z"), datetime(2018, 5, 17, 10, 55, 13) + ) + + self.assertEqual( + self.parser.parse_iso("20180517T105513-0700"), + datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), + ) + + # too many digits in date + with self.assertRaises(ParserError): + self.parser.parse_iso("201860517T105513Z") + + # too many digits in time + with self.assertRaises(ParserError): + self.parser.parse_iso("20180517T1055213Z") + class TzinfoParserTests(Chai): def setUp(self): From d15292318388373f43419390758a170ef6af5cfb Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 4 Aug 2019 11:29:33 -0400 Subject: [PATCH 238/649] Added back support for multiple subsecond tokens to align with format function --- arrow/parser.py | 6 +++- docs/index.rst | 2 +- tests/parser_tests.py | 69 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index a710b2407..3432b5213 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -228,6 +228,10 @@ def _generate_pattern_re(self, fmt): # Extract the bracketed expressions to be reinserted later. escaped_fmt = re.sub(self._ESCAPE_RE, "#", escaped_fmt) + # Any number of S is the same as one. + # TODO: allow users to specify the number of digits to parse + escaped_fmt = re.sub("S+", "S", escaped_fmt) + escaped_data = re.findall(self._ESCAPE_RE, fmt) fmt_pattern = escaped_fmt @@ -315,7 +319,7 @@ def _parse_token(self, token, value, parts): elif token == "S": # We have the *most significant* digits of an arbitrary-precision integer. # We want the six most significant digits as an integer, rounded. - # FIXME: add nanosecond support somehow? + # IDEA: add nanosecond support somehow? Need datetime support for it first. value = value.ljust(7, str("0")) # floating-point (IEEE-754) defaults to half-to-even rounding diff --git a/docs/index.rst b/docs/index.rst index f6f10c928..8273a6f62 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -357,7 +357,7 @@ Use the following tokens in parsing and formatting. Note that they're not the s +--------------------------------+--------------+-------------------------------------------+ | |s |0, 1, 2 ... 58, 59 | +--------------------------------+--------------+-------------------------------------------+ -|**Sub-second** |S |0, 02, 003, 000006, 123123123123... [#t3]_ | +|**Sub-second** |S... |0, 02, 003, 000006, 123123123123... [#t3]_ | +--------------------------------+--------------+-------------------------------------------+ |**Timezone** |ZZZ |Asia/Baku, Europe/Warsaw, GMT ... [#t4]_ | +--------------------------------+--------------+-------------------------------------------+ diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 5dc261c5f..f25bd1974 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -272,14 +272,44 @@ def test_parse_tz_name_zzz(self): self.parser.parse("2013-01-01 +1000", "YYYY-MM-DD ZZZ") def test_parse_subsecond(self): + # TODO: make both test_parse_subsecond functions in Parse and ParseISO + # tests use the same expected objects (use pytest fixtures) + self.expected = datetime(2013, 1, 1, 12, 30, 45, 900000) + self.assertEqual( + self.parser.parse("2013-01-01 12:30:45.9", "YYYY-MM-DD HH:mm:ss.S"), + self.expected, + ) - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) + self.expected = datetime(2013, 1, 1, 12, 30, 45, 980000) + self.assertEqual( + self.parser.parse("2013-01-01 12:30:45.98", "YYYY-MM-DD HH:mm:ss.SS"), + self.expected, + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987000) self.assertEqual( - self.parser.parse("2013-01-01 12:30:45.987654", "YYYY-MM-DD HH:mm:ss.S"), + self.parser.parse("2013-01-01 12:30:45.987", "YYYY-MM-DD HH:mm:ss.SSS"), self.expected, ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987600) self.assertEqual( - self.parser.parse_iso("2013-01-01 12:30:45.987654"), self.expected + self.parser.parse("2013-01-01 12:30:45.9876", "YYYY-MM-DD HH:mm:ss.SSSS"), + self.expected, + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987650) + self.assertEqual( + self.parser.parse("2013-01-01 12:30:45.98765", "YYYY-MM-DD HH:mm:ss.SSSSS"), + self.expected, + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) + self.assertEqual( + self.parser.parse( + "2013-01-01 12:30:45.987654", "YYYY-MM-DD HH:mm:ss.SSSSSS" + ), + self.expected, ) def test_parse_subsecond_rounding(self): @@ -328,8 +358,6 @@ def test_parse_with_extra_words_at_start_and_end_invalid(self): ("2016-05-16T04:05:06.789120ZblahZ", "YYYY-MM-DDThh:mm:ss.SZ"), ("2016-05-16T04:05:06.789120Zblah", "YYYY-MM-DDThh:mm:ss.SZ"), ("2016-05-16T04:05:06.789120blahZ", "YYYY-MM-DDThh:mm:ss.SZ"), - ("2016-05-16T04:05:06.789120Z", "YYYY-MM-DDThh:mm:ss.SSZ"), - ("2016-05-16T04:05:06.789120Z", "YYYY-MM-DDThh:mm:ss.SSSSSSZ"), ] for pair in input_format_pairs: @@ -495,7 +523,7 @@ def test_format_subsecond(self): def test_format_tz(self): - self.assertEqual(self.format_regex.findall("ZZ-Z"), ["ZZ", "Z"]) + self.assertEqual(self.format_regex.findall("ZZZ-ZZ-Z"), ["ZZZ", "ZZ", "Z"]) def test_format_am_pm(self): @@ -727,6 +755,35 @@ def test_YYYY_MM_DDTHH_mm_ss_SZ(self): datetime(2013, 2, 3, 4, 5, 6, 789120), ) + def test_parse_subsecond(self): + # TODO: make both test_parse_subsecond functions in Parse and ParseISO + # tests use the same expected objects (use pytest fixtures) + self.expected = datetime(2013, 1, 1, 12, 30, 45, 900000) + self.assertEqual(self.parser.parse_iso("2013-01-01 12:30:45.9"), self.expected) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 980000) + self.assertEqual(self.parser.parse_iso("2013-01-01 12:30:45.98"), self.expected) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987000) + self.assertEqual( + self.parser.parse_iso("2013-01-01 12:30:45.987"), self.expected + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987600) + self.assertEqual( + self.parser.parse_iso("2013-01-01 12:30:45.9876"), self.expected + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987650) + self.assertEqual( + self.parser.parse_iso("2013-01-01 12:30:45.98765"), self.expected + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) + self.assertEqual( + self.parser.parse_iso("2013-01-01 12:30:45.987654"), self.expected + ) + def test_gnu_date(self): """ regression tests for parsing output from GNU date(1) From f0ed46f2615dd62e0afda3f36358dec055fb5e94 Mon Sep 17 00:00:00 2001 From: Edward van Kuik Date: Sat, 3 Nov 2018 14:01:38 +0200 Subject: [PATCH 239/649] Added Afrikaans locale --- arrow/locales.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 99c171e7e..12cc26838 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1235,6 +1235,37 @@ class RussianLocale(SlavicBaseLocale): day_abbreviations = ["", "пн", "вт", "ср", "чт", "пт", "сб", "вс"] +class AfrikaansLocale(Locale): + + names = ['af', 'af_nl'] + + past = '{0} gelede' + future = 'in {0}' + + timeframes = { + 'now': 'nou', + 'seconds': 'sekondes', + 'minute': 'minuut', + 'minutes': '{0} minute', + 'hour': 'uur', + 'hours': '{0} ure', + 'day': 'een dag', + 'days': '{0} dae', + 'month': 'een maand', + 'months': '{0} maande', + 'year': 'een jaar', + 'years': '{0} jaar', + } + + month_names = ['', 'Januarie', 'Februarie', 'Maart', 'April', 'Mei', 'Junie', 'Julie', + 'Augustus', 'September', 'Oktober', 'November', 'Desember'] + month_abbreviations = ['', 'Jan', 'Feb', 'Mrt', 'Apr', 'Mei', 'Jun', 'Jul', 'Aug', + 'Sep', 'Okt', 'Nov', 'Des'] + + day_names = ['', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrydag', 'Saterdag', 'Sondag'] + day_abbreviations = ['', 'Ma', 'Di', 'Wo', 'Do', 'Vr', 'Za', 'So'] + + class BulgarianLocale(SlavicBaseLocale): names = ["bg", "bg_BG"] From 7c701d289bd4f6394145bc9299977919d26802ad Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Sun, 4 Aug 2019 22:54:10 +0100 Subject: [PATCH 240/649] black formatting fixes --- arrow/locales.py | 77 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 12cc26838..20017af4e 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1237,33 +1237,68 @@ class RussianLocale(SlavicBaseLocale): class AfrikaansLocale(Locale): - names = ['af', 'af_nl'] + names = ["af", "af_nl"] - past = '{0} gelede' - future = 'in {0}' + past = "{0} gelede" + future = "in {0}" timeframes = { - 'now': 'nou', - 'seconds': 'sekondes', - 'minute': 'minuut', - 'minutes': '{0} minute', - 'hour': 'uur', - 'hours': '{0} ure', - 'day': 'een dag', - 'days': '{0} dae', - 'month': 'een maand', - 'months': '{0} maande', - 'year': 'een jaar', - 'years': '{0} jaar', + "now": "nou", + "seconds": "sekondes", + "minute": "minuut", + "minutes": "{0} minute", + "hour": "uur", + "hours": "{0} ure", + "day": "een dag", + "days": "{0} dae", + "month": "een maand", + "months": "{0} maande", + "year": "een jaar", + "years": "{0} jaar", } - month_names = ['', 'Januarie', 'Februarie', 'Maart', 'April', 'Mei', 'Junie', 'Julie', - 'Augustus', 'September', 'Oktober', 'November', 'Desember'] - month_abbreviations = ['', 'Jan', 'Feb', 'Mrt', 'Apr', 'Mei', 'Jun', 'Jul', 'Aug', - 'Sep', 'Okt', 'Nov', 'Des'] + month_names = [ + "", + "Januarie", + "Februarie", + "Maart", + "April", + "Mei", + "Junie", + "Julie", + "Augustus", + "September", + "Oktober", + "November", + "Desember", + ] + month_abbreviations = [ + "", + "Jan", + "Feb", + "Mrt", + "Apr", + "Mei", + "Jun", + "Jul", + "Aug", + "Sep", + "Okt", + "Nov", + "Des", + ] - day_names = ['', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrydag', 'Saterdag', 'Sondag'] - day_abbreviations = ['', 'Ma', 'Di', 'Wo', 'Do', 'Vr', 'Za', 'So'] + day_names = [ + "", + "Maandag", + "Dinsdag", + "Woensdag", + "Donderdag", + "Vrydag", + "Saterdag", + "Sondag", + ] + day_abbreviations = ["", "Ma", "Di", "Wo", "Do", "Vr", "Za", "So"] class BulgarianLocale(SlavicBaseLocale): From 1b8e36bb5a0a4bee35197d446fcc681308c124ae Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 4 Aug 2019 17:54:13 -0400 Subject: [PATCH 241/649] Revamped regex of tz--work in progress --- arrow/parser.py | 32 +++++++++++++++++++++++++---- tests/parser_tests.py | 48 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 3432b5213..10b60c189 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -23,15 +23,23 @@ class DateTimeParser(object): _FORMAT_RE = 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)" ) + # TODO: add support for inner brackets like "2018-03-09 8 [[h]] 40" _ESCAPE_RE = re.compile(r"\[[^\[\]]*\]") - _ONE_OR_MORE_DIGIT_RE = re.compile(r"\d+") _ONE_OR_TWO_DIGIT_RE = re.compile(r"\d{1,2}") _ONE_OR_TWO_OR_THREE_DIGIT_RE = re.compile(r"\d{1,3}") + _ONE_OR_MORE_DIGIT_RE = re.compile(r"\d+") + _TWO_DIGIT_RE = re.compile(r"\d{2}") _THREE_DIGIT_RE = re.compile(r"\d{3}") _FOUR_DIGIT_RE = re.compile(r"\d{4}") - _TWO_DIGIT_RE = re.compile(r"\d{2}") + # _TZ_RE_ZZ = re.compile(r"^[+\-]\d{2}:(\d{2})?$|^ZZ?Z?$") + # _TZ_RE_Z = re.compile(r"^[+\-]\d{2}(\d{2})?$|^ZZ?Z?$") + + # _TZ_RE_ZZ = re.compile(r"^[+\-]\d{2}:(\d{2})?$|Z") + # _TZ_RE_Z = re.compile(r"^[+\-]\d{2}(\d{2})?$|Z") + _TZ_RE = re.compile(r"[+\-]?\d{2}:?(\d{2})?|Z") + # _TZ_RE = re.compile(r"[+\-]?\d{2}:?(\d{2})?") _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") _TIMESTAMP_RE = re.compile(r"^\d+\.?\d+$") # TODO: test timestamp thoroughly @@ -56,6 +64,8 @@ class DateTimeParser(object): "s": _ONE_OR_TWO_DIGIT_RE, "X": _TIMESTAMP_RE, "ZZZ": _TZ_NAME_RE, + # "ZZ": _TZ_RE_ZZ, + # "Z": _TZ_RE_Z, "ZZ": _TZ_RE, "Z": _TZ_RE, "S": _ONE_OR_MORE_DIGIT_RE, @@ -114,6 +124,8 @@ def parse_iso(self, datetime_string): has_time = has_space_divider or has_t_divider has_tz = False + # TODO: test basic format with timezone string without "+" + # TODO: add tests for all the new formats, especially basic format # IDEA: should YYYY MM DD style be accepted here? # date formats (ISO-8601 and others) to test against @@ -146,10 +158,22 @@ def parse_iso(self, datetime_string): else: date_string, time_string = datetime_string.split("T", 1) - time_parts = re.split("[+-]", time_string, 1) + time_parts = re.split(r"[+\-]", time_string, 1) colon_count = time_parts[0].count(":") is_basic_time_format = colon_count == 0 + tz_format = "Z" + + # tz offset is present + if len(time_parts) == 2: + tz_offset = time_parts[1] + + if ":" in tz_offset: + # TODO: add error message + if is_basic_time_format: + raise ParserError + + tz_format = "ZZ" has_tz = len(time_parts) > 1 has_hours = len(time_parts[0]) == 2 @@ -183,7 +207,7 @@ def parse_iso(self, datetime_string): if has_time and has_tz: # Add "Z" to format strings to indicate to _parse_tokens # that a timezone needs to be parsed - formats = ["{}Z".format(f) for f in formats] + formats = ["{}{}".format(f, tz_format) for f in formats] # TODO: make thrown error messages less cryptic and more informative return self._parse_multiformat(datetime_string, formats) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index f25bd1974..bec95c55a 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -533,6 +533,14 @@ def test_format_timestamp(self): self.assertEqual(self.format_regex.findall("X"), ["X"]) + def test_escape(self): + + escape_regex = parser.DateTimeParser._ESCAPE_RE + + self.assertEqual( + escape_regex.findall("2018-03-09 8 [h] 40 [hello]"), ["[h]", "[hello]"] + ) + def test_month_names(self): p = parser.DateTimeParser("en_us") @@ -553,16 +561,52 @@ def test_month_abbreviations(self): def test_digits(self): + self.assertEqual( + parser.DateTimeParser._ONE_OR_TWO_DIGIT_RE.findall("4-56"), ["4", "56"] + ) + self.assertEqual( + parser.DateTimeParser._ONE_OR_TWO_OR_THREE_DIGIT_RE.findall("4-56-789"), + ["4", "56", "789"], + ) + self.assertEqual( + parser.DateTimeParser._ONE_OR_MORE_DIGIT_RE.findall("4-56-789-1234-12345"), + ["4", "56", "789", "1234", "12345"], + ) self.assertEqual( parser.DateTimeParser._TWO_DIGIT_RE.findall("12-3-45"), ["12", "45"] ) self.assertEqual( - parser.DateTimeParser._FOUR_DIGIT_RE.findall("1234-56"), ["1234"] + parser.DateTimeParser._THREE_DIGIT_RE.findall("123-4-56"), ["123"] ) self.assertEqual( - parser.DateTimeParser._ONE_OR_TWO_DIGIT_RE.findall("4-56"), ["4", "56"] + parser.DateTimeParser._FOUR_DIGIT_RE.findall("1234-56"), ["1234"] ) + # def test_tz(self): + # tz_re = parser.DateTimeParser._TZ_RE + # + # self.assertEqual( + # tz_re.findall("-07:00"), ["-07", "00"] + # ) + + # self.assertEqual( + # tz_re.findall("+07:00"), ["+07:00"] + # ) + # + # self.assertEqual( + # tz_re.findall("-0700"), ["-0700"] + # ) + # + # self.assertEqual( + # tz_re.findall("+0700"), ["+0700"] + # ) + # + # self.assertEqual( + # tz_re.findall("Z"), ["Z"] + # ) + + # what about +Z? + class DateTimeParserISOTests(Chai): def setUp(self): From 4db714570a9ce58a08c72aa1477e9e7a48ed650c Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 4 Aug 2019 20:02:01 -0400 Subject: [PATCH 242/649] Replace hard coded timestamp with time.time() --- tests/util_tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/util_tests.py b/tests/util_tests.py index 9855a05a4..721697732 100644 --- a/tests/util_tests.py +++ b/tests/util_tests.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +import time + from chai import Chai from arrow import util @@ -6,7 +8,7 @@ class UtilTests(Chai): def test_is_timestamp(self): - timestamp_float = 1563047716.958061 + timestamp_float = time.time() timestamp_int = int(timestamp_float) self.assertTrue(util.is_timestamp(timestamp_int)) From f872d7a56c4399bb31c7f9a6bfcad6491d1d9de5 Mon Sep 17 00:00:00 2001 From: Charles-Axel Dein Date: Thu, 8 Aug 2019 14:41:18 +0200 Subject: [PATCH 243/649] Fix locale argument for factory.get() (#631) --- arrow/factory.py | 2 +- tests/factory_tests.py | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/arrow/factory.py b/arrow/factory.py index c4c7291d8..8cca6ac9b 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -150,7 +150,7 @@ def get(self, *args, **kwargs): """ arg_count = len(args) - locale = kwargs.get("locale", "en_us") + locale = kwargs.pop("locale", "en_us") tz = kwargs.get("tzinfo", None) # if kwargs given, send to constructor unless only tzinfo provided diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 635f0d0d4..acd4c36a0 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -257,15 +257,19 @@ def test_insufficient_kwargs(self): with self.assertRaises(TypeError): self.factory.get(year=2016, month=7) - def test_locale_kwarg_only(self): + def test_locale(self): + result = self.factory.get("2010", "YYYY", locale="ja") + self.assertEqual( + result._datetime, datetime(2010, 1, 1, 0, 0, 0, 0, tzinfo=tz.tzutc()) + ) - with self.assertRaises(TypeError): - self.factory.get(locale="ja") + def test_locale_kwarg_only(self): + res = self.factory.get(locale="ja") + self.assertEqual(res.tzinfo, tz.tzutc()) def test_locale_with_tzinfo(self): - - with self.assertRaises(TypeError): - self.factory.get(locale="ja", tzinfo=tz.gettz("Asia/Tokyo")) + res = self.factory.get(locale="ja", tzinfo=tz.gettz("Asia/Tokyo")) + self.assertEqual(res.tzinfo, tz.gettz("Asia/Tokyo")) class UtcNowTests(Chai): From 88a12e1704fc315472b5b2a502bcb509c2426762 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Thu, 8 Aug 2019 18:56:44 +0100 Subject: [PATCH 244/649] Update HISTORY.md with 0.14.5 --- HISTORY.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index c430308dc..78425d82f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ ## History +### 0.14.5 + +- [NEW] Added Afrikaans locale. +- [CHANGE] Removed deprecated replace shift functionality. +- [FIX] Fixed bug that occurred when factory.get() was passed a locale kwarg. + ### 0.14.4 - [FIX] Fixed a regression in 0.14.3 that prevented a tzinfo argument of type string to be passed to the `get()` function. Functionality such as `arrow.get("2019072807", "YYYYMMDDHH", tzinfo="UTC")` should work as normal again. From 4e7c9333acde2a321bf8d44003d07daecd657e5e Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 8 Aug 2019 19:27:52 -0400 Subject: [PATCH 245/649] up version --- arrow/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/_version.py b/arrow/_version.py index 224f1fb74..141826d55 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.14.4" +__version__ = "0.14.5" From 35e33f7a5927047dbda6e04043de9387071ed552 Mon Sep 17 00:00:00 2001 From: Choco02 <41271075+Choco02@users.noreply.github.com> Date: Fri, 9 Aug 2019 14:46:50 -0300 Subject: [PATCH 246/649] Updates to BrazilianPortugueseLocale (#632) * BrazilianPortugueseLocale updated --- arrow/locales.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 20017af4e..81a7bad78 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1753,6 +1753,66 @@ class BrazilianPortugueseLocale(PortugueseLocale): past = "faz {0}" + future = "em {0}" + + timeframes = { + "now": "agora", + "seconds": "segundos", + "minute": "um minuto", + "minutes": "{0} minutos", + "hour": "uma hora", + "hours": "{0} horas", + "day": "um dia", + "days": "{0} dias", + "month": "um mês", + "months": "{0} meses", + "year": "um ano", + "years": "{0} anos", + } + + month_names = [ + "", + "Janeiro", + "Fevereiro", + "Março", + "Abril", + "Maio", + "Junho", + "Julho", + "Agosto", + "Setembro", + "Outubro", + "Novembro", + "Dezembro", + ] + month_abbreviations = [ + "", + "Jan", + "Fev", + "Mar", + "Abr", + "Mai", + "Jun", + "Jul", + "Ago", + "Set", + "Out", + "Nov", + "Dez", + ] + + day_names = [ + "", + "Segunda-feira", + "Terça-feira", + "Quarta-feira", + "Quinta-feira", + "Sexta-feira", + "Sábado", + "Domingo", + ] + day_abbreviations = ["", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab", "Dom"] + class TagalogLocale(Locale): From 4c360cb24aff86d573a508c5d015c156c23d346e Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Fri, 9 Aug 2019 16:57:23 -0400 Subject: [PATCH 247/649] Tweaked tzinfo parser regex and added some TODOs --- arrow/parser.py | 53 ++++++++++++++++++++++--------------------- tests/parser_tests.py | 3 ++- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 10b60c189..6f69aee1b 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -32,14 +32,9 @@ class DateTimeParser(object): _TWO_DIGIT_RE = re.compile(r"\d{2}") _THREE_DIGIT_RE = re.compile(r"\d{3}") _FOUR_DIGIT_RE = re.compile(r"\d{4}") - # _TZ_RE_ZZ = re.compile(r"^[+\-]\d{2}:(\d{2})?$|^ZZ?Z?$") - # _TZ_RE_Z = re.compile(r"^[+\-]\d{2}(\d{2})?$|^ZZ?Z?$") - - # _TZ_RE_ZZ = re.compile(r"^[+\-]\d{2}:(\d{2})?$|Z") - # _TZ_RE_Z = re.compile(r"^[+\-]\d{2}(\d{2})?$|Z") - - _TZ_RE = re.compile(r"[+\-]?\d{2}:?(\d{2})?|Z") - # _TZ_RE = re.compile(r"[+\-]?\d{2}:?(\d{2})?") + # TODO: +07 is not possible with colon, fix regex + _TZ_RE_ZZ = re.compile(r"[+\-]\d{2}:(\d{2})?|Z") + _TZ_RE_Z = re.compile(r"[+\-]\d{2}(\d{2})?|Z") _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") _TIMESTAMP_RE = re.compile(r"^\d+\.?\d+$") # TODO: test timestamp thoroughly @@ -64,10 +59,8 @@ class DateTimeParser(object): "s": _ONE_OR_TWO_DIGIT_RE, "X": _TIMESTAMP_RE, "ZZZ": _TZ_NAME_RE, - # "ZZ": _TZ_RE_ZZ, - # "Z": _TZ_RE_Z, - "ZZ": _TZ_RE, - "Z": _TZ_RE, + "ZZ": _TZ_RE_ZZ, + "Z": _TZ_RE_Z, "S": _ONE_OR_MORE_DIGIT_RE, } @@ -148,7 +141,15 @@ def parse_iso(self, datetime_string): "YYYY", ] + # TODO: add test that accounts for someone adding +Z or -Z to the datetime string vs just Z if has_time: + # TODO: write a test for this (more than one Z in datetime string) + if "Z" in datetime_string and datetime_string.count("Z") > 1: + # TODO: improve error message + raise ParserError( + "More than one 'Z' provided in the datetime string. Please pass in a single Z to denote the UTC timezone." + ) + # Z is ignored entirely because fromdatetime defaults to UTC in arrow.py if datetime_string[-1] == "Z": datetime_string = datetime_string[:-1] @@ -205,7 +206,7 @@ def parse_iso(self, datetime_string): # TODO: reduce set of date formats for basic? test earlier? if has_time and has_tz: - # Add "Z" to format strings to indicate to _parse_tokens + # Add "Z" to format strings to indicate to _parse_token # that a timezone needs to be parsed formats = ["{}{}".format(f, tz_format) for f in formats] @@ -299,13 +300,11 @@ def _generate_pattern_re(self, fmt): # Reference: https://stackoverflow.com/q/14232931/3820660 starting_word_boundary = r"(? Date: Sat, 10 Aug 2019 06:11:28 -0400 Subject: [PATCH 248/649] Added a comment to tzinfoparser --- arrow/parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/arrow/parser.py b/arrow/parser.py index 6f69aee1b..6720ea1cc 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -456,6 +456,7 @@ def _choice_re(choices, flags=0): class TzinfoParser(object): + # TODO: align this with the TZ_RE_Z and TZ_RE_ZZ above # TODO: test this REGEX _TZINFO_RE = re.compile(r"^([+\-])?(\d{2}):?(\d{2})?$") From 2b1a7669a75c793fa3fd9f72d674af2a17afbabf Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 10 Aug 2019 14:40:22 -0400 Subject: [PATCH 249/649] Updated HISTORY to reflect deprecation changes in 0.14.5 (#635) --- HISTORY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 78425d82f..8cbcacccd 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -3,8 +3,8 @@ ### 0.14.5 - [NEW] Added Afrikaans locale. -- [CHANGE] Removed deprecated replace shift functionality. -- [FIX] Fixed bug that occurred when factory.get() was passed a locale kwarg. +- [CHANGE] Removed deprecated `replace` shift functionality. Users looking to pass plural properties to the `replace` function to shift values should use `shift` instead. +- [FIX] Fixed bug that occurred when `factory.get()` was passed a locale kwarg. ### 0.14.4 From 4ed43aeb101daaec4e55dd553262410ecaf3c36a Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 13 Aug 2019 17:18:06 +0300 Subject: [PATCH 250/649] Finalized regex changes --- arrow/parser.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 6720ea1cc..4e797a70b 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -32,9 +32,12 @@ class DateTimeParser(object): _TWO_DIGIT_RE = re.compile(r"\d{2}") _THREE_DIGIT_RE = re.compile(r"\d{3}") _FOUR_DIGIT_RE = re.compile(r"\d{4}") - # TODO: +07 is not possible with colon, fix regex - _TZ_RE_ZZ = re.compile(r"[+\-]\d{2}:(\d{2})?|Z") - _TZ_RE_Z = re.compile(r"[+\-]\d{2}(\d{2})?|Z") + # https://regex101.com/r/ifOZxu/4 + _TZ_RE_Z = re.compile(r"([\+\-])(\d{2})(?:(\d{2}))?|Z") + # https://regex101.com/r/ifOZxu/5 + _TZ_RE_ZZ = re.compile(r"([\+\-])(\d{2})(?:\:(\d{2}))?|Z") + # _TZ_RE_ZZ = re.compile(r"[\+\-]\d{2}:(\d{2})?|Z") + # _TZ_RE_Z = re.compile(r"[\+\-]\d{2}(\d{2})?|Z") _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") _TIMESTAMP_RE = re.compile(r"^\d+\.?\d+$") # TODO: test timestamp thoroughly @@ -159,7 +162,7 @@ def parse_iso(self, datetime_string): else: date_string, time_string = datetime_string.split("T", 1) - time_parts = re.split(r"[+\-]", time_string, 1) + time_parts = re.split(r"[\+\-]", time_string, 1) colon_count = time_parts[0].count(":") is_basic_time_format = colon_count == 0 @@ -176,11 +179,12 @@ def parse_iso(self, datetime_string): tz_format = "ZZ" + # TODO: use regex to determine if something is basic format has_tz = len(time_parts) > 1 has_hours = len(time_parts[0]) == 2 has_minutes = colon_count == 1 or len(time_parts[0]) == 4 has_seconds = colon_count == 2 or len(time_parts[0]) == 6 - has_subseconds = re.search("[.,]", time_parts[0]) + has_subseconds = re.search(r"[\.,]", time_parts[0]) if has_subseconds: time_string = "HH:mm:ss{}S".format(has_subseconds.group()) @@ -255,7 +259,7 @@ def _generate_pattern_re(self, fmt): # Any number of S is the same as one. # TODO: allow users to specify the number of digits to parse - escaped_fmt = re.sub("S+", "S", escaped_fmt) + escaped_fmt = re.sub(r"S+", "S", escaped_fmt) escaped_data = re.findall(self._ESCAPE_RE, fmt) @@ -458,7 +462,8 @@ def _choice_re(choices, flags=0): class TzinfoParser(object): # TODO: align this with the TZ_RE_Z and TZ_RE_ZZ above # TODO: test this REGEX - _TZINFO_RE = re.compile(r"^([+\-])?(\d{2}):?(\d{2})?$") + # https://regex101.com/r/ifOZxu/3 + _TZINFO_RE = re.compile(r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$") @classmethod def parse(cls, tzinfo_string): From 87684c142e65db38a96c9e279ddb46a640f6a2cf Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 13 Aug 2019 18:16:30 +0300 Subject: [PATCH 251/649] Added remaining regex tests --- arrow/parser.py | 11 +++++----- tests/parser_tests.py | 49 ++++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 4e797a70b..f35f7254d 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -32,12 +32,11 @@ class DateTimeParser(object): _TWO_DIGIT_RE = re.compile(r"\d{2}") _THREE_DIGIT_RE = re.compile(r"\d{3}") _FOUR_DIGIT_RE = re.compile(r"\d{4}") + # TODO: test someone passing +Z or -Z # https://regex101.com/r/ifOZxu/4 - _TZ_RE_Z = re.compile(r"([\+\-])(\d{2})(?:(\d{2}))?|Z") + _TZ_Z_RE = re.compile(r"([\+\-])(\d{2})(?:(\d{2}))?|Z") # https://regex101.com/r/ifOZxu/5 - _TZ_RE_ZZ = re.compile(r"([\+\-])(\d{2})(?:\:(\d{2}))?|Z") - # _TZ_RE_ZZ = re.compile(r"[\+\-]\d{2}:(\d{2})?|Z") - # _TZ_RE_Z = re.compile(r"[\+\-]\d{2}(\d{2})?|Z") + _TZ_ZZ_RE = re.compile(r"([\+\-])(\d{2})(?:\:(\d{2}))?|Z") _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") _TIMESTAMP_RE = re.compile(r"^\d+\.?\d+$") # TODO: test timestamp thoroughly @@ -62,8 +61,8 @@ class DateTimeParser(object): "s": _ONE_OR_TWO_DIGIT_RE, "X": _TIMESTAMP_RE, "ZZZ": _TZ_NAME_RE, - "ZZ": _TZ_RE_ZZ, - "Z": _TZ_RE_Z, + "ZZ": _TZ_ZZ_RE, + "Z": _TZ_Z_RE, "S": _ONE_OR_MORE_DIGIT_RE, } diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 9975ff9d3..74f5d0fdf 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -582,30 +582,31 @@ def test_digits(self): parser.DateTimeParser._FOUR_DIGIT_RE.findall("1234-56"), ["1234"] ) - # def test_tz(self): - # tz_re = parser.DateTimeParser._TZ_RE - # - # self.assertEqual( - # tz_re.findall("-07:00"), ["-07", "00"] - # ) - # - # self.assertEqual( - # tz_re.findall("+07:00"), ["+07:00"] - # ) - # - # self.assertEqual( - # tz_re.findall("-0700"), ["-0700"] - # ) - # - # self.assertEqual( - # tz_re.findall("+0700"), ["+0700"] - # ) - # - # self.assertEqual( - # tz_re.findall("Z"), ["Z"] - # ) - - # what about +Z? + def test_tz(self): + tz_z_re = parser.DateTimeParser._TZ_Z_RE + self.assertEqual(tz_z_re.findall("-0700"), [("-", "07", "00")]) + self.assertEqual(tz_z_re.findall("+07"), [("+", "07", "")]) + self.assertTrue(tz_z_re.search("15/01/2019T04:05:06.789120Z") is not None) + self.assertTrue(tz_z_re.search("15/01/2019T04:05:06.789120") is None) + + tz_zz_re = parser.DateTimeParser._TZ_ZZ_RE + self.assertEqual(tz_zz_re.findall("-07:00"), [("-", "07", "00")]) + self.assertEqual(tz_zz_re.findall("+07"), [("+", "07", "")]) + self.assertTrue(tz_zz_re.search("15/01/2019T04:05:06.789120Z") is not None) + self.assertTrue(tz_zz_re.search("15/01/2019T04:05:06.789120") is None) + + tz_name_re = parser.DateTimeParser._TZ_NAME_RE + self.assertEqual(tz_name_re.findall("Europe/Warsaw"), ["Europe/Warsaw"]) + self.assertEqual(tz_name_re.findall("GMT"), ["GMT"]) + + def test_timestamp(self): + timestamp_re = parser.DateTimeParser._TIMESTAMP_RE + self.assertEqual( + timestamp_re.findall("1565707550.452729"), ["1565707550.452729"] + ) + self.assertEqual(timestamp_re.findall("1565707550"), ["1565707550"]) + self.assertEqual(timestamp_re.findall("1565707550."), []) + self.assertEqual(timestamp_re.findall(".1565707550"), []) class DateTimeParserISOTests(Chai): From 62e8ced0f3bf37a818d9341cd811a29a9f70931d Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 13 Aug 2019 18:25:39 +0300 Subject: [PATCH 252/649] Added a few comments --- arrow/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index f35f7254d..e1d30f2e2 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -23,7 +23,6 @@ class DateTimeParser(object): _FORMAT_RE = 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)" ) - # TODO: add support for inner brackets like "2018-03-09 8 [[h]] 40" _ESCAPE_RE = re.compile(r"\[[^\[\]]*\]") _ONE_OR_TWO_DIGIT_RE = re.compile(r"\d{1,2}") @@ -32,7 +31,6 @@ class DateTimeParser(object): _TWO_DIGIT_RE = re.compile(r"\d{2}") _THREE_DIGIT_RE = re.compile(r"\d{3}") _FOUR_DIGIT_RE = re.compile(r"\d{4}") - # TODO: test someone passing +Z or -Z # https://regex101.com/r/ifOZxu/4 _TZ_Z_RE = re.compile(r"([\+\-])(\d{2})(?:(\d{2}))?|Z") # https://regex101.com/r/ifOZxu/5 @@ -185,6 +183,8 @@ def parse_iso(self, datetime_string): has_seconds = colon_count == 2 or len(time_parts[0]) == 6 has_subseconds = re.search(r"[\.,]", time_parts[0]) + # Add tests for someone mixing basic format with colon-separated + if has_subseconds: time_string = "HH:mm:ss{}S".format(has_subseconds.group()) elif has_seconds: From 11f5751e6dd6b1854829ab325ff208d79c9bee2d Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 15 Aug 2019 17:09:56 +0300 Subject: [PATCH 253/649] Removed instances of replace with plural properties from docs (#637) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 36550f0ac..b9a16be33 100644 --- a/README.rst +++ b/README.rst @@ -83,7 +83,7 @@ Example Usage >>> utc - >>> utc = utc.replace(hours=-1) + >>> utc = utc.shift(hours=-1) >>> utc From 46f30b2a6908be36283de5c4daac943cb5bbe565 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Fri, 16 Aug 2019 17:07:19 +0100 Subject: [PATCH 254/649] Make error messages clearer and add extra DDDD test --- arrow/parser.py | 26 +++++++++++--------------- tests/parser_tests.py | 4 ++++ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 3432b5213..f73448c70 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -102,11 +102,16 @@ def parse_iso(self, datetime_string): has_t_divider = "T" in datetime_string num_spaces = datetime_string.count(" ") - if (has_space_divider and num_spaces != 1) or ( - has_t_divider and num_spaces > 0 - ): + if has_space_divider and num_spaces != 1: raise ParserError( - "Expected an ISO 8601-like string, but was given '{}'. Try passing in a format string to resolve this.".format( + "Expected an ISO 8601-like string, but was given '{}' which contains multiple spaces. Try passing in a format string to resolve this.".format( + datetime_string + ) + ) + + if has_t_divider and num_spaces > 0: + raise ParserError( + "Expected an ISO 8601-like string, but was given '{}' which contains \"T\" separator and spaces. Try passing in a format string to resolve this.".format( datetime_string ) ) @@ -115,7 +120,6 @@ def parse_iso(self, datetime_string): has_tz = False # TODO: add tests for all the new formats, especially basic format - # IDEA: should YYYY MM DD style be accepted here? # date formats (ISO-8601 and others) to test against formats = [ "YYYY-MM-DD", @@ -185,7 +189,6 @@ def parse_iso(self, datetime_string): # that a timezone needs to be parsed formats = ["{}Z".format(f) for f in formats] - # TODO: make thrown error messages less cryptic and more informative return self._parse_multiformat(datetime_string, formats) def parse(self, datetime_string, fmt): @@ -198,9 +201,7 @@ def parse(self, datetime_string, fmt): match = fmt_pattern_re.search(datetime_string) if match is None: raise ParserError( - "Failed to match '{}' when parsing '{}'".format( - fmt_pattern_re.pattern, datetime_string - ) + "Failed to match '{}' when parsing '{}'".format(fmt, datetime_string) ) parts = {} @@ -354,7 +355,6 @@ def _build_datetime(parts): tz_utc = tz.tzutc() return datetime.fromtimestamp(timestamp, tz=tz_utc) - # TODO: add tests for this! day_of_year = parts.get("day_of_year") if day_of_year: @@ -380,10 +380,6 @@ def _build_datetime(parts): ) ) - # TODO: write test for 2015-366 - # TODO: should we throw an error or mimic datetime? - # datetime.strptime("2015-366", "%Y-%j") - # Changes year: datetime.datetime(2016, 1, 1, 0, 0) parts["year"] = dt.year parts["month"] = dt.month parts["day"] = dt.day @@ -420,7 +416,7 @@ def _parse_multiformat(self, string, formats): if _datetime is None: raise ParserError( - "Could not match input '{}' to any of the supported formats: {}".format( + "Could not match input '{}' to any of the formats provided: {}".format( string, ", ".join(formats) ) ) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index f25bd1974..e90b5d940 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -582,6 +582,10 @@ def test_YYYY_DDDD(self): with self.assertRaises(ParserError): self.parser.parse_iso("1998-456") + # datetime.strptime("2015-366", "%Y-%j") + # Changes year: datetime.datetime(2016, 1, 1, 0, 0) + self.assertEqual(self.parser.parse_iso("2015-366"), datetime(2016, 1, 1)) + def test_YYYY_DDDD_HH_mm_ssZ(self): self.assertEqual( From 4e36c9ad8ed9022268ec3e3d7a9a313b48ae3fd3 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Fri, 16 Aug 2019 21:56:48 +0300 Subject: [PATCH 255/649] Fixed a critical bug with X token and float timestamps; revamped basic format to use regex --- arrow/arrow.py | 2 +- arrow/parser.py | 78 ++++++++++++++++++++---------------------- docs/index.rst | 6 ++-- tests/arrow_tests.py | 2 +- tests/parser_tests.py | 79 +++++++++++++++++++++++++++++++++++-------- 5 files changed, 106 insertions(+), 61 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 19bbbe0b4..cb52a1fa9 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1352,7 +1352,7 @@ def _get_iteration_params(cls, end, limit): if end is None: if limit is None: - raise Exception("one of 'end' or 'limit' is required") + raise ValueError("one of 'end' or 'limit' is required") return cls.max, limit diff --git a/arrow/parser.py b/arrow/parser.py index e1d30f2e2..f613c2c54 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -36,7 +36,10 @@ class DateTimeParser(object): # https://regex101.com/r/ifOZxu/5 _TZ_ZZ_RE = re.compile(r"([\+\-])(\d{2})(?:\:(\d{2}))?|Z") _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") + # TODO: test timestamp with natural language processing. I think we may have to remove the ^...$ _TIMESTAMP_RE = re.compile(r"^\d+\.?\d+$") + # https://regex101.com/r/LDMBVi/2 + _TIME_RE = re.compile(r"^(\d{2})(?:\:?(\d{2}))?(?:\:?(\d{2}))?(?:([\.\,])(\d+))?$") # TODO: test timestamp thoroughly # TODO: test new regular expressions @@ -121,6 +124,7 @@ def parse_iso(self, datetime_string): # TODO: add tests for all the new formats, especially basic format # IDEA: should YYYY MM DD style be accepted here? + # NOTE: YYYYMM is omitted to avoid confusion with YYMMDD (no longer part of ISO 8601, but is still often used) # date formats (ISO-8601 and others) to test against formats = [ "YYYY-MM-DD", @@ -141,73 +145,66 @@ def parse_iso(self, datetime_string): "YYYY", ] - # TODO: add test that accounts for someone adding +Z or -Z to the datetime string vs just Z if has_time: - # TODO: write a test for this (more than one Z in datetime string) - if "Z" in datetime_string and datetime_string.count("Z") > 1: - # TODO: improve error message - raise ParserError( - "More than one 'Z' provided in the datetime string. Please pass in a single Z to denote the UTC timezone." - ) - - # Z is ignored entirely because fromdatetime defaults to UTC in arrow.py - if datetime_string[-1] == "Z": - datetime_string = datetime_string[:-1] 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"[\+\-]", time_string, 1) - colon_count = time_parts[0].count(":") + time_parts = re.split(r"[\+\-Z]", time_string, 1, re.IGNORECASE) + # TODO: is it a bug that we are just checking the timeparts for colons? this allows users to mix basic and extended like: 20130203 04:05:06.78912Z + time_colon_count = time_parts[0].count(":") - is_basic_time_format = colon_count == 0 + is_basic_time_format = time_colon_count == 0 tz_format = "Z" - # tz offset is present - if len(time_parts) == 2: - tz_offset = time_parts[1] + # use 'ZZ' token instead since tz offset is present in non-basic format + if len(time_parts) == 2 and ":" in time_parts[1]: + # TODO: should we throw an error if someone mixes non-basic tz (e.g. 07:00) with a basic datetime string? + # I thought so at first, but then I thought it was too much error checking. - if ":" in tz_offset: - # TODO: add error message - if is_basic_time_format: - raise ParserError + tz_format = "ZZ" - tz_format = "ZZ" + time_components = self._TIME_RE.match(time_parts[0]) - # TODO: use regex to determine if something is basic format - has_tz = len(time_parts) > 1 - has_hours = len(time_parts[0]) == 2 - has_minutes = colon_count == 1 or len(time_parts[0]) == 4 - has_seconds = colon_count == 2 or len(time_parts[0]) == 6 - has_subseconds = re.search(r"[\.,]", time_parts[0]) + if time_components is None: + raise ParserError( + "Invalid time component provided. Please specify a format or provide a valid time component in the basic or extended ISO 8601 time format.".format() + ) - # Add tests for someone mixing basic format with colon-separated + hours, minutes, seconds, subseconds_sep, subseconds = ( + time_components.groups() + ) + + has_tz = len(time_parts) == 2 + has_hours = hours is not None + has_minutes = minutes is not None + has_seconds = seconds is not None + has_subseconds = subseconds is not None + + time_sep = "" if is_basic_time_format else ":" if has_subseconds: - time_string = "HH:mm:ss{}S".format(has_subseconds.group()) + time_string = "HH{time_sep}mm{time_sep}ss{subseconds_sep}S".format( + time_sep=time_sep, subseconds_sep=subseconds_sep + ) elif has_seconds: - time_string = "HH:mm:ss" + time_string = "HH{time_sep}mm{time_sep}ss".format(time_sep=time_sep) elif has_minutes: - time_string = "HH:mm" + time_string = "HH{time_sep}mm".format(time_sep=time_sep) elif has_hours: time_string = "HH" else: raise ParserError( - "Invalid time component provided. Please specify a format or provide a time in the form 'HH:mm:ss.S', 'HH:mm:ss', 'HH:mm', or 'HH'." + "Invalid time component provided. Please specify a format or provide a valid time component in the basic or extended ISO 8601 time format." ) - if is_basic_time_format: - time_string = time_string.replace(":", "") - if has_space_divider: formats = ["{} {}".format(f, time_string) for f in formats] else: formats = ["{}T{}".format(f, time_string) for f in formats] - # TODO: reduce set of date formats for basic? test earlier? - if has_time and has_tz: # Add "Z" to format strings to indicate to _parse_token # that a timezone needs to be parsed @@ -360,7 +357,7 @@ def _parse_token(self, token, value, parts): parts["microsecond"] = int(value[:6]) + rounding elif token == "X": - parts["timestamp"] = int(value) + parts["timestamp"] = float(value) elif token in ["ZZZ", "ZZ", "Z"]: parts["tzinfo"] = TzinfoParser.parse(value) @@ -459,8 +456,7 @@ def _choice_re(choices, flags=0): class TzinfoParser(object): - # TODO: align this with the TZ_RE_Z and TZ_RE_ZZ above - # TODO: test this REGEX + # TODO: test against full timezone DB # https://regex101.com/r/ifOZxu/3 _TZINFO_RE = re.compile(r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$") diff --git a/docs/index.rst b/docs/index.rst index 8273a6f62..14a62f1e1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -361,11 +361,11 @@ Use the following tokens in parsing and formatting. Note that they're not the s +--------------------------------+--------------+-------------------------------------------+ |**Timezone** |ZZZ |Asia/Baku, Europe/Warsaw, GMT ... [#t4]_ | +--------------------------------+--------------+-------------------------------------------+ -| |ZZ |-07:00, -06:00 ... +06:00, +07:00 | +| |ZZ |-07:00, -06:00 ... +06:00, +07:00, +08, Z | +--------------------------------+--------------+-------------------------------------------+ -| |Z |-0700, -0600 ... +0600, +0700 | +| |Z |-0700, -0600 ... +0600, +0700, +08, Z | +--------------------------------+--------------+-------------------------------------------+ -|**Timestamp** |X |1381685817 | +|**Timestamp** |X |1381685817, 1381685817.915482 ... | +--------------------------------+--------------+-------------------------------------------+ .. rubric:: Footnotes diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index c88959b0d..87c2048d0 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1812,5 +1812,5 @@ def test_get_iteration_params(self): ) self.assertEqual(arrow.Arrow._get_iteration_params(100, 120), (100, 120)) - with self.assertRaises(Exception): + with self.assertRaises(ValueError): arrow.Arrow._get_iteration_params(None, None) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 74f5d0fdf..db92ab848 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -133,7 +133,7 @@ def test_YY_and_YYYY_format_list(self): ) # regression test for issue #447 - def test_parse_timestamp_token(self): + def test_timestamp_format_list(self): # should not match on the "X" token self.assertEqual( self.parser.parse( @@ -200,9 +200,13 @@ def test_parse_year_two_digit(self): def test_parse_timestamp(self): tz_utc = tz.tzutc() - timestamp = int(time.time()) - self.expected = datetime.fromtimestamp(timestamp, tz=tz_utc) - self.assertEqual(self.parser.parse(str(timestamp), "X"), self.expected) + int_timestamp = int(time.time()) + self.expected = datetime.fromtimestamp(int_timestamp, tz=tz_utc) + self.assertEqual(self.parser.parse(str(int_timestamp), "X"), self.expected) + + float_timestamp = time.time() + self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) + self.assertEqual(self.parser.parse(str(float_timestamp), "X"), self.expected) def test_parse_names(self): @@ -269,7 +273,13 @@ def test_parse_tz_name_zzz(self): # note that offsets are not timezones with self.assertRaises(ParserError): - self.parser.parse("2013-01-01 +1000", "YYYY-MM-DD ZZZ") + self.parser.parse("2013-01-01 12:30:45.9+1000", "YYYY-MM-DDZZZ") + + with self.assertRaises(ParserError): + self.parser.parse("2013-01-01 12:30:45.9+10:00", "YYYY-MM-DDZZZ") + + with self.assertRaises(ParserError): + self.parser.parse("2013-01-01 12:30:45.9-10", "YYYY-MM-DDZZZ") def test_parse_subsecond(self): # TODO: make both test_parse_subsecond functions in Parse and ParseISO @@ -407,7 +417,7 @@ def test_parse_with_extra_words_at_start_and_end_valid(self): self.assertEqual( self.parser.parse( - "Meet me at 2016-05-16T04:05:06.789120 on Tuesday", + "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), @@ -415,7 +425,7 @@ def test_parse_with_extra_words_at_start_and_end_valid(self): self.assertEqual( self.parser.parse( - "Meet me at 2016-05-16 04:05:06.789120 on Tuesday", + "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), @@ -634,6 +644,11 @@ def test_YYYY_DDDD_HH_mm_ssZ(self): datetime(2013, 2, 5, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600)), ) + self.assertEqual( + self.parser.parse_iso("2013-036 04:05:06Z"), + datetime(2013, 2, 5, 4, 5, 6, tzinfo=tz.tzutc()), + ) + def test_YYYY_MM_DDDD(self): with self.assertRaises(ParserError): self.parser.parse_iso("2014-05-125") @@ -792,14 +807,34 @@ def test_YYYY_MM_DDTHH_mm_ss_SZ(self): datetime(2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzoffset(None, 3600)), ) - # parse_iso sets tzinfo to None if Z is passed, so a default datetime - # object is sufficient to compare against. - # Arrow adds +00:00 when get() is called directly and tzinfo is None self.assertEqual( self.parser.parse_iso("2013-02-03 04:05:06.78912Z"), - datetime(2013, 2, 3, 4, 5, 6, 789120), + datetime(2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzutc()), ) + def test_invalid_Z(self): + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912z") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912zz") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912Zz") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912ZZ") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912+Z") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912-Z") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912 Z") + def test_parse_subsecond(self): # TODO: make both test_parse_subsecond functions in Parse and ParseISO # tests use the same expected objects (use pytest fixtures) @@ -873,8 +908,8 @@ def test_parse_iso_with_extra_words_at_start_and_end_invalid(self): "2016-05-16T04:05:06.789120ZblahZ", "2016-05-16T04:05:06.789120Zblah", "2016-05-16T04:05:06.789120blahZ", - "Meet me at 2016-05-16T04:05:06.789120 on Tuesday", - "Meet me at 2016-05-16 04:05:06.789120 on Tuesday", + "Meet me at 2016-05-16T04:05:06.789120 at the restaurant.", + "Meet me at 2016-05-16 04:05:06.789120 at the restaurant.", ] for ti in test_inputs: @@ -913,7 +948,8 @@ def test_iso8601_basic_format(self): ) self.assertEqual( - self.parser.parse_iso("20180517T105513Z"), datetime(2018, 5, 17, 10, 55, 13) + self.parser.parse_iso("20180517T105513Z"), + datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzutc()), ) self.assertEqual( @@ -921,6 +957,9 @@ def test_iso8601_basic_format(self): datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), ) + # ordinal in basic format: YYYYDDDD + self.assertEqual(self.parser.parse_iso("1998136"), datetime(1998, 5, 16)) + # too many digits in date with self.assertRaises(ParserError): self.parser.parse_iso("201860517T105513Z") @@ -947,11 +986,21 @@ def test_parse_utc(self): def test_parse_iso(self): - # TODO: add tests! self.assertEqual(self.parser.parse("01:00"), tz.tzoffset(None, 3600)) + self.assertEqual( + self.parser.parse("11:35"), tz.tzoffset(None, 11 * 3600 + 2100) + ) self.assertEqual(self.parser.parse("+01:00"), tz.tzoffset(None, 3600)) self.assertEqual(self.parser.parse("-01:00"), tz.tzoffset(None, -3600)) + self.assertEqual(self.parser.parse("0100"), tz.tzoffset(None, 3600)) + self.assertEqual(self.parser.parse("+0100"), tz.tzoffset(None, 3600)) + self.assertEqual(self.parser.parse("-0100"), tz.tzoffset(None, -3600)) + + self.assertEqual(self.parser.parse("01"), tz.tzoffset(None, 3600)) + self.assertEqual(self.parser.parse("+01"), tz.tzoffset(None, 3600)) + self.assertEqual(self.parser.parse("-01"), tz.tzoffset(None, -3600)) + def test_parse_str(self): self.assertEqual(self.parser.parse("US/Pacific"), tz.gettz("US/Pacific")) From 6f13d10e3367217b171cbe2a8f1dff9eb4344917 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 17 Aug 2019 00:45:07 +0300 Subject: [PATCH 256/649] Added tests and removed stripping of whitespace --- arrow/parser.py | 14 +++------- docs/index.rst | 3 ++- tests/factory_tests.py | 25 +++++++++++++----- tests/parser_tests.py | 59 ++++++++++++++++++++++++++++-------------- 4 files changed, 64 insertions(+), 37 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index f613c2c54..fb5b1fdde 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -31,18 +31,14 @@ class DateTimeParser(object): _TWO_DIGIT_RE = re.compile(r"\d{2}") _THREE_DIGIT_RE = re.compile(r"\d{3}") _FOUR_DIGIT_RE = re.compile(r"\d{4}") - # https://regex101.com/r/ifOZxu/4 _TZ_Z_RE = re.compile(r"([\+\-])(\d{2})(?:(\d{2}))?|Z") - # https://regex101.com/r/ifOZxu/5 _TZ_ZZ_RE = re.compile(r"([\+\-])(\d{2})(?:\:(\d{2}))?|Z") _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") - # TODO: test timestamp with natural language processing. I think we may have to remove the ^...$ + # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will + # break cases like "15 Jul 2000" and a format list (see issue #447) _TIMESTAMP_RE = re.compile(r"^\d+\.?\d+$") - # https://regex101.com/r/LDMBVi/2 _TIME_RE = re.compile(r"^(\d{2})(?:\:?(\d{2}))?(?:\:?(\d{2}))?(?:([\.\,])(\d+))?$") - # TODO: test timestamp thoroughly - # TODO: test new regular expressions _BASE_INPUT_RE_MAP = { "YYYY": _FOUR_DIGIT_RE, "YY": _TWO_DIGIT_RE, @@ -101,8 +97,7 @@ def __init__(self, locale="en_us", cache_size=0): # TODO: since we support more than ISO-8601, we should rename this function # IDEA: break into multiple functions def parse_iso(self, datetime_string): - # strip leading and trailing whitespace - datetime_string = datetime_string.strip() + # TODO: add a flag to normalize whitespace (useful in logs, ref issue #421) has_space_divider = " " in datetime_string has_t_divider = "T" in datetime_string @@ -170,7 +165,7 @@ def parse_iso(self, datetime_string): if time_components is None: raise ParserError( - "Invalid time component provided. Please specify a format or provide a valid time component in the basic or extended ISO 8601 time format.".format() + "Invalid time component provided. Please specify a format or provide a valid time component in the basic or extended ISO 8601 time format." ) hours, minutes, seconds, subseconds_sep, subseconds = ( @@ -457,7 +452,6 @@ def _choice_re(choices, flags=0): class TzinfoParser(object): # TODO: test against full timezone DB - # https://regex101.com/r/ifOZxu/3 _TZINFO_RE = re.compile(r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$") @classmethod diff --git a/docs/index.rst b/docs/index.rst index 14a62f1e1..78fcea57c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -365,7 +365,7 @@ Use the following tokens in parsing and formatting. Note that they're not the s +--------------------------------+--------------+-------------------------------------------+ | |Z |-0700, -0600 ... +0600, +0700, +08, Z | +--------------------------------+--------------+-------------------------------------------+ -|**Timestamp** |X |1381685817, 1381685817.915482 ... | +|**Timestamp** |X |1381685817, 1381685817.915482 ... [#t5]_ | +--------------------------------+--------------+-------------------------------------------+ .. rubric:: Footnotes @@ -374,6 +374,7 @@ Use the following tokens in parsing and formatting. Note that they're not the s .. [#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 +.. [#t5] this token cannot be used for parsing timestamps out of natural language strings due to compatibility reasons Escaping Formats ~~~~~~~~~~~~~~~~ diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 4ea4d9acf..361e4257d 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -6,6 +6,7 @@ from dateutil import tz from arrow import factory, util +from arrow.parser import ParserError def assertDtEqual(dt1, dt2, within=10): @@ -45,17 +46,27 @@ def test_struct_time(self): def test_one_arg_timestamp(self): - timestamp = 12345 - timestamp_dt = datetime.utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) + int_timestamp = int(time.time()) + timestamp_dt = datetime.utcfromtimestamp(int_timestamp).replace( + tzinfo=tz.tzutc() + ) + + self.assertEqual(self.factory.get(int_timestamp), timestamp_dt) - self.assertEqual(self.factory.get(timestamp), timestamp_dt) + with self.assertRaises(ParserError): + self.factory.get(str(int_timestamp)) + + float_timestamp = time.time() + timestamp_dt = datetime.utcfromtimestamp(float_timestamp).replace( + tzinfo=tz.tzutc() + ) - timestamp = 123.45 - timestamp_dt = datetime.utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) + self.assertEqual(self.factory.get(float_timestamp), timestamp_dt) - self.assertEqual(self.factory.get(timestamp), timestamp_dt) + with self.assertRaises(ParserError): + self.factory.get(str(float_timestamp)) - # Issue 216 + # Regression test for issue #216 timestamp = 99999999999999999999999999 # Python 3 raises `OverflowError`, Python 2 raises `ValueError` with self.assertRaises((OverflowError, ValueError)): diff --git a/tests/parser_tests.py b/tests/parser_tests.py index db92ab848..f0ef85825 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -208,6 +208,20 @@ def test_parse_timestamp(self): self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) self.assertEqual(self.parser.parse(str(float_timestamp), "X"), self.expected) + # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will + # break cases like "15 Jul 2000" and a format list (see issue #447) + with self.assertRaises(ParserError): + natural_lang_string = "Meet me at {} at the restaurant.".format( + float_timestamp + ) + self.parser.parse(natural_lang_string, "X") + + with self.assertRaises(ParserError): + self.parser.parse("1565982019.", "X") + + with self.assertRaises(ParserError): + self.parser.parse(".1565982019", "X") + def test_parse_names(self): self.expected = datetime(2012, 1, 1) @@ -618,6 +632,32 @@ def test_timestamp(self): self.assertEqual(timestamp_re.findall("1565707550."), []) self.assertEqual(timestamp_re.findall(".1565707550"), []) + def test_time(self): + time_re = parser.DateTimeParser._TIME_RE + time_seperators = [":", ""] + + for sep in time_seperators: + self.assertEqual(time_re.findall("12"), [("12", "", "", "", "")]) + self.assertEqual( + time_re.findall("12{sep}35".format(sep=sep)), [("12", "35", "", "", "")] + ) + self.assertEqual( + time_re.findall("12{sep}35{sep}46".format(sep=sep)), + [("12", "35", "46", "", "")], + ) + self.assertEqual( + time_re.findall("12{sep}35{sep}46.952313".format(sep=sep)), + [("12", "35", "46", ".", "952313")], + ) + self.assertEqual( + time_re.findall("12{sep}35{sep}46,952313".format(sep=sep)), + [("12", "35", "46", ",", "952313")], + ) + + self.assertEqual(time_re.findall("12:"), []) + self.assertEqual(time_re.findall("12:35:46."), []) + self.assertEqual(time_re.findall("12:35:46,"), []) + class DateTimeParserISOTests(Chai): def setUp(self): @@ -916,25 +956,6 @@ def test_parse_iso_with_extra_words_at_start_and_end_invalid(self): with self.assertRaises(ParserError): self.parser.parse_iso(ti) - def test_parse_iso_with_leading_and_trailing_whitespace(self): - self.assertEqual(self.parser.parse_iso(" 2016"), datetime(2016, 1, 1)) - - self.assertEqual(self.parser.parse_iso("2016 "), datetime(2016, 1, 1)) - - self.assertEqual( - self.parser.parse_iso(" 2016 "), datetime(2016, 1, 1) - ) - - self.assertEqual( - self.parser.parse_iso(" 2016-05-16 04:05:06.789120 "), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) - - self.assertEqual( - self.parser.parse_iso(" 2016-05-16T04:05:06.789120 "), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) - def test_iso8601_basic_format(self): self.assertEqual(self.parser.parse_iso("20180517"), datetime(2018, 5, 17)) From d9cb79e5a183c3204d9c20b8f63309277cf154c6 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 17 Aug 2019 01:03:11 +0300 Subject: [PATCH 257/649] Increment version --- arrow/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/_version.py b/arrow/_version.py index 141826d55..9da2f8fcc 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.14.5" +__version__ = "0.15.0" From d7e083bef18713925992cea760613749c364893d Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 17 Aug 2019 03:12:43 +0300 Subject: [PATCH 258/649] Fixed float timestamp on py27, removed warnings, changed parser error to inherit from value error --- arrow/factory.py | 26 -------------------------- arrow/parser.py | 3 +-- tests/parser_tests.py | 8 ++++++-- 3 files changed, 7 insertions(+), 30 deletions(-) diff --git a/arrow/factory.py b/arrow/factory.py index 246ab5ce6..ef8e8972d 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -9,7 +9,6 @@ from __future__ import absolute_import import calendar -import warnings from datetime import date, datetime, tzinfo from time import struct_time @@ -20,21 +19,6 @@ from arrow.util import is_timestamp, isstr -class ArrowParseWarning(DeprecationWarning): - """Raised when arrow.get() is passed a string with no formats and matches incorrectly - on one of the default formats. - - e.g. - arrow.get('blabla2016') -> - arrow.get('13/4/2045') -> - - In version 0.15.0 this warning will become a ParserError. - """ - - -warnings.simplefilter("always", ArrowParseWarning) - - class ArrowFactory(object): """ A factory for generating :class:`Arrow ` objects. @@ -195,11 +179,6 @@ def get(self, *args, **kwargs): # (str) -> parse. elif isstr(arg): - warnings.warn( - "The .get() parsing method without a format string will parse more strictly in version 0.15.0." - "See https://github.com/crsmithdev/arrow/issues/612 for more details.", - ArrowParseWarning, - ) dt = parser.DateTimeParser(locale).parse_iso(arg) return self.type.fromdatetime(dt, tz) @@ -242,11 +221,6 @@ def get(self, *args, **kwargs): # (str, format) -> parse. elif isstr(arg_1) and (isstr(arg_2) or isinstance(arg_2, list)): - warnings.warn( - "The .get() parsing method with a format string will parse more strictly in version 0.15.0." - "See https://github.com/crsmithdev/arrow/issues/612 for more details.", - ArrowParseWarning, - ) dt = parser.DateTimeParser(locale).parse(args[0], args[1]) return self.type.fromdatetime(dt, tzinfo=tz) diff --git a/arrow/parser.py b/arrow/parser.py index 8a1bb181b..8223c3466 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -14,7 +14,7 @@ from backports.functools_lru_cache import lru_cache # pragma: no cover -class ParserError(RuntimeError): +class ParserError(ValueError): pass @@ -98,7 +98,6 @@ def __init__(self, locale="en_us", cache_size=0): # IDEA: break into multiple functions def parse_iso(self, datetime_string): # TODO: add a flag to normalize whitespace (useful in logs, ref issue #421) - has_space_divider = " " in datetime_string has_t_divider = "T" in datetime_string diff --git a/tests/parser_tests.py b/tests/parser_tests.py index f79d3cb29..917b69fab 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -202,11 +202,15 @@ def test_parse_timestamp(self): tz_utc = tz.tzutc() int_timestamp = int(time.time()) self.expected = datetime.fromtimestamp(int_timestamp, tz=tz_utc) - self.assertEqual(self.parser.parse(str(int_timestamp), "X"), self.expected) + self.assertEqual( + self.parser.parse("{:d}".format(int_timestamp), "X"), self.expected + ) float_timestamp = time.time() self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - self.assertEqual(self.parser.parse(str(float_timestamp), "X"), self.expected) + self.assertEqual( + self.parser.parse("{:f}".format(float_timestamp), "X"), self.expected + ) # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will # break cases like "15 Jul 2000" and a format list (see issue #447) From 992038d6c67bd121c14184d4d9da39561d40026d Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 17 Aug 2019 11:39:10 +0300 Subject: [PATCH 259/649] Added tests and cleaned up TODOs --- arrow/parser.py | 7 +------ tests/parser_tests.py | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 8223c3466..742c793f4 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -119,8 +119,6 @@ def parse_iso(self, datetime_string): has_time = has_space_divider or has_t_divider has_tz = False - # TODO: test basic format with timezone string without "+" - # TODO: add tests for all the new formats, especially basic format # NOTE: YYYYMM is omitted to avoid confusion with YYMMDD (no longer part of ISO 8601, but is still often used) # date formats (ISO-8601 and others) to test against @@ -151,7 +149,7 @@ def parse_iso(self, datetime_string): date_string, time_string = datetime_string.split("T", 1) time_parts = re.split(r"[\+\-Z]", time_string, 1, re.IGNORECASE) - # TODO: is it a bug that we are just checking the timeparts for colons? this allows users to mix basic and extended like: 20130203 04:05:06.78912Z + # TODO: should we prevent mixing basic and extended formats? would need to ensure that dates, times, and timezones are in same format time_colon_count = time_parts[0].count(":") is_basic_time_format = time_colon_count == 0 @@ -159,9 +157,6 @@ def parse_iso(self, datetime_string): # use 'ZZ' token instead since tz offset is present in non-basic format if len(time_parts) == 2 and ":" in time_parts[1]: - # TODO: should we throw an error if someone mixes non-basic tz (e.g. 07:00) with a basic datetime string? - # I thought so at first, but then I thought it was too much error checking. - tz_format = "ZZ" time_components = self._TIME_RE.match(time_parts[0]) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 917b69fab..9c1da2edc 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -972,8 +972,8 @@ def test_iso8601_basic_format(self): ) self.assertEqual( - self.parser.parse_iso("20180517T105513.84"), - datetime(2018, 5, 17, 10, 55, 13, 840000), + self.parser.parse_iso("20180517T105513.843456"), + datetime(2018, 5, 17, 10, 55, 13, 843456), ) self.assertEqual( @@ -981,14 +981,47 @@ def test_iso8601_basic_format(self): datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzutc()), ) + self.assertEqual( + self.parser.parse_iso("20180517T105513.843456-0700"), + datetime(2018, 5, 17, 10, 55, 13, 843456, tzinfo=tz.tzoffset(None, -25200)), + ) + self.assertEqual( self.parser.parse_iso("20180517T105513-0700"), datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), ) + self.assertEqual( + self.parser.parse_iso("20180517T105513-07"), + datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), + ) + + # mixing formats--this may raise a ParserError in the future + self.assertEqual( + self.parser.parse_iso("2018-05-17T105513-0700"), + datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), + ) + + self.assertEqual( + self.parser.parse_iso("20180517T10:55:13-07:00"), + datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), + ) + + self.assertEqual( + self.parser.parse_iso("20180517T105513-07:00"), + datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), + ) + # ordinal in basic format: YYYYDDDD self.assertEqual(self.parser.parse_iso("1998136"), datetime(1998, 5, 16)) + # timezone requires +- seperator + with self.assertRaises(ParserError): + self.parser.parse_iso("20180517T1055130700") + + with self.assertRaises(ParserError): + self.parser.parse_iso("20180517T10551307") + # too many digits in date with self.assertRaises(ParserError): self.parser.parse_iso("201860517T105513Z") From fca652290d9944652b1c97200e9eff12f559197e Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 17 Aug 2019 15:16:47 +0300 Subject: [PATCH 260/649] Added ordinal tests and added new error type to fix exception bubbling issue with ordinal dates --- arrow/parser.py | 31 +++++++++++++++++++------------ tests/parser_tests.py | 31 ++++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 742c793f4..266b439f7 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -18,6 +18,15 @@ class ParserError(ValueError): pass +# Allows for ParserErrors to be propagated from _build_datetime() +# when day_of_year errors occur. +# Before this, the ParserErrors were caught by the try/except in +# _parse_multiformat() and the appropriate error message was not +# transmitted to the user. +class ParserMatchError(ParserError): + pass + + class DateTimeParser(object): _FORMAT_RE = re.compile( @@ -104,14 +113,14 @@ def parse_iso(self, datetime_string): num_spaces = datetime_string.count(" ") if has_space_divider and num_spaces != 1: raise ParserError( - "Expected an ISO 8601-like string, but was given '{}' which contains multiple spaces. Try passing in a format string to resolve this.".format( + "Expected an ISO 8601-like string, but was given '{}', which contains multiple spaces. Try passing in a format string to resolve this.".format( datetime_string ) ) if has_t_divider and num_spaces > 0: raise ParserError( - "Expected an ISO 8601-like string, but was given '{}' which contains \"T\" separator and spaces. Try passing in a format string to resolve this.".format( + "Expected an ISO 8601-like string, but was given '{}', which contains a 'T' separator and spaces. Try passing in a format string to resolve this.".format( datetime_string ) ) @@ -214,7 +223,7 @@ def parse(self, datetime_string, fmt): match = fmt_pattern_re.search(datetime_string) if match is None: - raise ParserError( + raise ParserMatchError( "Failed to match '{}' when parsing '{}'".format(fmt, datetime_string) ) @@ -363,23 +372,23 @@ def _build_datetime(parts): timestamp = parts.get("timestamp") - if timestamp: + if timestamp is not None: tz_utc = tz.tzutc() return datetime.fromtimestamp(timestamp, tz=tz_utc) day_of_year = parts.get("day_of_year") - if day_of_year: + if day_of_year is not None: year = parts.get("year") month = parts.get("month") if year is None: raise ParserError( - "Year component is required with the DDD and DDDD tokens" + "Year component is required with the DDD and DDDD tokens." ) if month is not None: raise ParserError( - "Month component is not allowed with the DDD and DDDD tokens" + "Month component is not allowed with the DDD and DDDD tokens." ) date_string = "{}-{}".format(year, day_of_year) @@ -387,9 +396,7 @@ def _build_datetime(parts): dt = datetime.strptime(date_string, "%Y-%j") except ValueError: raise ParserError( - "Expected a valid day of year, but received '{}'".format( - day_of_year - ) + "The provided day of year '{}' is invalid.".format(day_of_year) ) parts["year"] = dt.year @@ -423,12 +430,12 @@ def _parse_multiformat(self, string, formats): try: _datetime = self.parse(string, fmt) break - except ParserError: + except ParserMatchError: pass if _datetime is None: raise ParserError( - "Could not match input '{}' to any of the formats provided: {}".format( + "Could not match input '{}' to any of the following formats: {}".format( string, ", ".join(formats) ) ) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 9c1da2edc..bd13b6eb0 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -9,7 +9,7 @@ from dateutil import tz from arrow import parser -from arrow.parser import DateTimeParser, ParserError +from arrow.parser import DateTimeParser, ParserError, ParserMatchError class DateTimeParserTests(Chai): @@ -22,7 +22,7 @@ def test_parse_multiformat(self): mock_datetime = self.mock() - self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserError) + self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserMatchError) self.expect(self.parser.parse).args("str", "fmt_b").returns(mock_datetime) result = self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) @@ -31,8 +31,8 @@ def test_parse_multiformat(self): def test_parse_multiformat_all_fail(self): - self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserError) - self.expect(self.parser.parse).args("str", "fmt_b").raises(ParserError) + self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserMatchError) + self.expect(self.parser.parse).args("str", "fmt_b").raises(ParserMatchError) with self.assertRaises(ParserError): self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) @@ -174,12 +174,12 @@ def test_parse_unrecognized_token(self): def test_parse_parse_no_match(self): - with self.assertRaises(parser.ParserError): + with self.assertRaises(ParserError): self.parser.parse("01-01", "YYYY-MM-DD") def test_parse_separators(self): - with self.assertRaises(parser.ParserError): + with self.assertRaises(ParserError): self.parser.parse("1403549231", "YYYY-MM-DD") def test_parse_numbers(self): @@ -499,6 +499,7 @@ def test_parse_YYYY_MM_DDDD(self): with self.assertRaises(ParserError): self.parser.parse("2015-01-009", "YYYY-MM-DDDD") + # year is required with the DDD and DDDD tokens def test_parse_DDD_only(self): with self.assertRaises(ParserError): self.parser.parse("5", "DDD") @@ -681,9 +682,21 @@ def test_YYYY_DDDD(self): with self.assertRaises(ParserError): self.parser.parse_iso("1998-456") - # datetime.strptime("2015-366", "%Y-%j") - # Changes year: datetime.datetime(2016, 1, 1, 0, 0) - self.assertEqual(self.parser.parse_iso("2015-366"), datetime(2016, 1, 1)) + # 2016 is a leap year, so Feb 29 exists (leap day) + self.assertEqual(self.parser.parse_iso("2016-059"), datetime(2016, 2, 28)) + self.assertEqual(self.parser.parse_iso("2016-060"), datetime(2016, 2, 29)) + self.assertEqual(self.parser.parse_iso("2016-061"), datetime(2016, 3, 1)) + + # 2017 is not a leap year, so Feb 29 does not exist + self.assertEqual(self.parser.parse_iso("2017-059"), datetime(2017, 2, 28)) + self.assertEqual(self.parser.parse_iso("2017-060"), datetime(2017, 3, 1)) + self.assertEqual(self.parser.parse_iso("2017-061"), datetime(2017, 3, 2)) + + # Since 2016 is a leap year, the 366th day falls in the same year + self.assertEqual(self.parser.parse_iso("2016-366"), datetime(2016, 12, 31)) + + # Since 2017 is not a leap year, the 366th day falls in the next year + self.assertEqual(self.parser.parse_iso("2017-366"), datetime(2018, 1, 1)) def test_YYYY_DDDD_HH_mm_ssZ(self): From d63fd39da2c160d05568eb5411501e9249ef0ca1 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 17 Aug 2019 15:21:01 +0300 Subject: [PATCH 261/649] Consolidate and clean up errors for spacing --- arrow/parser.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 266b439f7..0d7103d5a 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -111,16 +111,11 @@ def parse_iso(self, datetime_string): has_t_divider = "T" in datetime_string num_spaces = datetime_string.count(" ") - if has_space_divider and num_spaces != 1: + if (has_space_divider and num_spaces != 1) or ( + has_t_divider and num_spaces > 0 + ): raise ParserError( - "Expected an ISO 8601-like string, but was given '{}', which contains multiple spaces. Try passing in a format string to resolve this.".format( - datetime_string - ) - ) - - if has_t_divider and num_spaces > 0: - raise ParserError( - "Expected an ISO 8601-like string, but was given '{}', which contains a 'T' separator and spaces. Try passing in a format string to resolve this.".format( + "Expected an ISO 8601-like string, but was given '{}'. Try passing in a format string to resolve this.".format( datetime_string ) ) From e99837382b6763b77588693e6aacd370d06eb4f8 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 17 Aug 2019 15:22:47 +0300 Subject: [PATCH 262/649] Consolidate and clean up errors for spacing --- arrow/parser.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 0d7103d5a..1ded17aa9 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -111,9 +111,7 @@ def parse_iso(self, datetime_string): has_t_divider = "T" in datetime_string num_spaces = datetime_string.count(" ") - if (has_space_divider and num_spaces != 1) or ( - has_t_divider and num_spaces > 0 - ): + if has_space_divider and num_spaces != 1 or has_t_divider and num_spaces > 0: raise ParserError( "Expected an ISO 8601-like string, but was given '{}'. Try passing in a format string to resolve this.".format( datetime_string From 9940542e3b521ea446f1a50a4e057b626fb3f7e1 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 17 Aug 2019 15:33:55 +0300 Subject: [PATCH 263/649] Cleaned up a comment --- arrow/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 1ded17aa9..ea3f6c6f4 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -201,8 +201,8 @@ def parse_iso(self, datetime_string): formats = ["{}T{}".format(f, time_string) for f in formats] if has_time and has_tz: - # Add "Z" to format strings to indicate to _parse_token - # that a timezone needs to be parsed + # Add "Z" or "ZZ" to the format strings to indicate to + # _parse_token() that a timezone needs to be parsed formats = ["{}{}".format(f, tz_format) for f in formats] return self._parse_multiformat(datetime_string, formats) From 62cda843369760a68c82ffae6163d7b71609795a Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 18 Aug 2019 14:27:05 +0300 Subject: [PATCH 264/649] Move Macedonian locale to slavic base (#641) * Moving macedonian locale to slavic base and adding tests * cleaning up tests --- arrow/locales.py | 152 ++++++++++++++++++++--------------------- tests/locales_tests.py | 29 ++++++++ 2 files changed, 105 insertions(+), 76 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 81a7bad78..4e540f0bf 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1433,6 +1433,82 @@ class UkrainianLocale(SlavicBaseLocale): day_abbreviations = ["", "пн", "вт", "ср", "чт", "пт", "сб", "нд"] +class MacedonianLocale(SlavicBaseLocale): + names = ["mk", "mk_mk"] + + past = "пред {0}" + future = "за {0}" + + timeframes = { + "now": "сега", + "seconds": "секунди", + "minute": "една минута", + "minutes": ["{0} минута", "{0} минути", "{0} минути"], + "hour": "еден саат", + "hours": ["{0} саат", "{0} саати", "{0} саати"], + "day": "еден ден", + "days": ["{0} ден", "{0} дена", "{0} дена"], + "month": "еден месец", + "months": ["{0} месец", "{0} месеци", "{0} месеци"], + "year": "една година", + "years": ["{0} година", "{0} години", "{0} години"], + } + + meridians = {"am": "дп", "pm": "пп", "AM": "претпладне", "PM": "попладне"} + + month_names = [ + "", + "Јануари", + "Февруари", + "Март", + "Април", + "Мај", + "Јуни", + "Јули", + "Август", + "Септември", + "Октомври", + "Ноември", + "Декември", + ] + month_abbreviations = [ + "", + "Јан.", + " Фев.", + " Мар.", + " Апр.", + " Мај", + " Јун.", + " Јул.", + " Авг.", + " Септ.", + " Окт.", + " Ноем.", + " Декем.", + ] + + day_names = [ + "", + "Понеделник", + " Вторник", + " Среда", + " Четврток", + " Петок", + " Сабота", + " Недела", + ] + day_abbreviations = [ + "", + "Пон.", + " Вт.", + " Сре.", + " Чет.", + " Пет.", + " Саб.", + " Нед.", + ] + + class _DeutschLocaleCommonMixin(object): past = "vor {0}" @@ -2843,82 +2919,6 @@ class FarsiLocale(Locale): day_abbreviations = ["", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] -class MacedonianLocale(Locale): - names = ["mk", "mk_mk"] - - past = "пред {0}" - future = "за {0}" - - timeframes = { - "now": "сега", - "seconds": "секунди", - "minute": "една минута", - "minutes": "{0} минути", - "hour": "еден саат", - "hours": "{0} саати", - "day": "еден ден", - "days": "{0} дена", - "month": "еден месец", - "months": "{0} месеци", - "year": "една година", - "years": "{0} години", - } - - meridians = {"am": "дп", "pm": "пп", "AM": "претпладне", "PM": "попладне"} - - month_names = [ - "", - "Јануари", - "Февруари", - "Март", - "Април", - "Мај", - "Јуни", - "Јули", - "Август", - "Септември", - "Октомври", - "Ноември", - "Декември", - ] - month_abbreviations = [ - "", - "Јан.", - " Фев.", - " Мар.", - " Апр.", - " Мај", - " Јун.", - " Јул.", - " Авг.", - " Септ.", - " Окт.", - " Ноем.", - " Декем.", - ] - - day_names = [ - "", - "Понеделник", - " Вторник", - " Среда", - " Четврток", - " Петок", - " Сабота", - " Недела", - ] - day_abbreviations = [ - "", - "Пон.", - " Вт.", - " Сре.", - " Чет.", - " Пет.", - " Саб.", - " Нед.", - ] - - class HebrewLocale(Locale): names = ["he", "he_IL"] diff --git a/tests/locales_tests.py b/tests/locales_tests.py index c1a8912a3..0abf2083d 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -323,6 +323,35 @@ def test_plurals2(self): self.assertEqual(locale._format_timeframe("minutes", 25), "25 минути") +class MacedonianLocaleTests(Chai): + def test_plurals_mk(self): + + locale = locales.MacedonianLocale() + + # time + self.assertEqual(locale._format_relative("сега", "now", 0), "сега") + + # Hours + self.assertEqual(locale._format_timeframe("hours", 0), "0 саати") + self.assertEqual(locale._format_timeframe("hours", 1), "1 саат") + self.assertEqual(locale._format_timeframe("hours", 2), "2 саати") + self.assertEqual(locale._format_timeframe("hours", 4), "4 саати") + self.assertEqual(locale._format_timeframe("hours", 5), "5 саати") + self.assertEqual(locale._format_timeframe("hours", 21), "21 саат") + self.assertEqual(locale._format_timeframe("hours", 22), "22 саати") + self.assertEqual(locale._format_timeframe("hours", 25), "25 саати") + + # Minutes + self.assertEqual(locale._format_timeframe("minutes", 0), "0 минути") + self.assertEqual(locale._format_timeframe("minutes", 1), "1 минута") + self.assertEqual(locale._format_timeframe("minutes", 2), "2 минути") + self.assertEqual(locale._format_timeframe("minutes", 4), "4 минути") + self.assertEqual(locale._format_timeframe("minutes", 5), "5 минути") + self.assertEqual(locale._format_timeframe("minutes", 21), "21 минута") + self.assertEqual(locale._format_timeframe("minutes", 22), "22 минути") + self.assertEqual(locale._format_timeframe("minutes", 25), "25 минути") + + class HebrewLocaleTests(Chai): def test_couple_of_timeframe(self): locale = locales.HebrewLocale() From 56f665d986802886bddaaa4ac4cfc2b6746f6f03 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 18 Aug 2019 14:47:51 +0300 Subject: [PATCH 265/649] Fix pytz and tzinfo string bugs in Arrow factory (#640) * Fixed bug with Arrow instantiation and pytz timezone objects * Fixed coverage and upgraded pre-commit hooks and dependencies * Fixed bug with tzinfo string passed into get being ignored * Updated docs to reflect replace changes; updated contributing section; updated pre-commit hook --- .pre-commit-config.yaml | 15 ++++++---- README.rst | 6 ++-- README.rst.new | 0 arrow/arrow.py | 32 ++++++++++++++------- arrow/factory.py | 18 ++++++++---- requirements.txt | 12 ++++---- tests/arrow_tests.py | 63 +++++++++++++++++++++++++++++++++++++++-- tests/factory_tests.py | 12 +++++++- 8 files changed, 125 insertions(+), 33 deletions(-) create mode 100644 README.rst.new diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 825d4569d..22aa58715 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,17 +2,21 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.2.3 + rev: v2.3.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: fix-encoding-pragma exclude: ^arrow/_version.py + - id: requirements-txt-fixer + - id: check-ast - id: check-yaml + - id: check-case-conflict + - id: check-docstring-first + - id: check-merge-conflict - id: debug-statements - - id: requirements-txt-fixer - repo: https://github.com/asottile/seed-isort-config - rev: v1.9.1 + rev: v1.9.2 hooks: - id: seed-isort-config - repo: https://github.com/pre-commit/mirrors-isort @@ -20,13 +24,14 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v1.20.1 + rev: v1.22.1 hooks: - id: pyupgrade - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.4.0 + rev: v1.4.1 hooks: - id: python-no-eval + - id: python-check-blanket-noqa - id: rst-backticks - repo: https://github.com/python/black rev: 19.3b0 diff --git a/README.rst b/README.rst index b9a16be33..84eaa2982 100644 --- a/README.rst +++ b/README.rst @@ -32,7 +32,7 @@ What? Arrow is a Python library that offers a sensible, human-friendly approach to creating, manipulating, formatting and converting dates, times, and timestamps. It implements and updates the datetime type, plugging gaps in functionality, and provides an intelligent module API that supports many common creation scenarios. Simply put, it helps you work with dates and times with fewer imports and a lot less code. -Arrow is heavily inspired by `moment.js `_ and `requests `_. +Arrow is heavily inspired by `moment.js `_ and `requests `_. Why? ---- @@ -52,7 +52,7 @@ Features - Supports Python 2.7, 3.5, 3.6, 3.7 and 3.8 - Timezone-aware & UTC by default - Provides super-simple creation options for many common input scenarios -- Updated :code:`replace` method with support for relative offsets, including weeks +- :code:`shift` method with support for relative offsets, including weeks - Formats and parses strings automatically - Partial support for ISO-8601 - Timezone conversion @@ -119,4 +119,4 @@ For full documentation, please visit `arrow.readthedocs.io `_ and then fork `the repository `_ on GitHub to begin making changes. If you would like to help with localization, please see `locales.py `_ for what locales are currently supported. If you are helping with code, make sure to add tests to ensure that a bug was fixed or the feature works as intended. +Contributions are welcome for both code and localizations. To get started, find an issue or feature to tackle on `the issue tracker `_ and then fork `this repository `_ on GitHub to begin making changes. If you would like to help with localizations, please see `locales.py `_ to see what locales are currently supported. If you are helping with code, make sure to add a few tests to ensure that the bug was fixed or the feature works as intended. diff --git a/README.rst.new b/README.rst.new new file mode 100644 index 000000000..e69de29bb diff --git a/arrow/arrow.py b/arrow/arrow.py index 08b9b571d..3a6f2ef8b 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -9,7 +9,8 @@ import calendar import sys -from datetime import datetime, timedelta, tzinfo +from datetime import datetime, timedelta +from datetime import tzinfo as dt_tzinfo from math import trunc from dateutil import tz as dateutil_tz @@ -59,10 +60,17 @@ class Arrow(object): def __init__( self, year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None ): - - if util.isstr(tzinfo): + if tzinfo is None: + tzinfo = dateutil_tz.tzutc() + # detect that tzinfo is a pytz object (issue #626) + elif ( + isinstance(tzinfo, dt_tzinfo) + and hasattr(tzinfo, "localize") + and tzinfo.zone + ): + tzinfo = parser.TzinfoParser.parse(tzinfo.zone) + elif util.isstr(tzinfo): tzinfo = parser.TzinfoParser.parse(tzinfo) - tzinfo = tzinfo if tzinfo is not None else dateutil_tz.tzutc() self._datetime = datetime( year, month, day, hour, minute, second, microsecond, tzinfo @@ -84,7 +92,8 @@ def now(cls, tzinfo=None): """ - tzinfo = tzinfo if tzinfo is not None else dateutil_tz.tzlocal() + if tzinfo is None: + tzinfo = dateutil_tz.tzlocal() dt = datetime.now(tzinfo) return cls( @@ -138,7 +147,8 @@ def fromtimestamp(cls, timestamp, tzinfo=None): """ - tzinfo = tzinfo if tzinfo is not None else dateutil_tz.tzlocal() + if tzinfo is None: + tzinfo = dateutil_tz.tzlocal() timestamp = cls._get_timestamp_from_input(timestamp) dt = datetime.fromtimestamp(timestamp, tzinfo) @@ -219,7 +229,8 @@ def fromdate(cls, date, tzinfo=None): :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to UTC. """ - tzinfo = tzinfo if tzinfo is not None else dateutil_tz.tzutc() + if tzinfo is None: + tzinfo = dateutil_tz.tzutc() return cls(date.year, date.month, date.day, tzinfo=tzinfo) @@ -241,7 +252,8 @@ def strptime(cls, date_str, fmt, tzinfo=None): """ dt = datetime.strptime(date_str, fmt) - tzinfo = tzinfo if tzinfo is not None else dt.tzinfo + if tzinfo is None: + tzinfo = dt.tzinfo return cls( dt.year, @@ -677,7 +689,7 @@ def to(self, tz): """ - if not isinstance(tz, tzinfo): + if not isinstance(tz, dt_tzinfo): tz = parser.TzinfoParser.parse(tz) dt = self._datetime.astimezone(tz) @@ -1286,7 +1298,7 @@ def _get_tzinfo(tz_expr): if tz_expr is None: return dateutil_tz.tzutc() - if isinstance(tz_expr, tzinfo): + if isinstance(tz_expr, dt_tzinfo): return tz_expr else: try: diff --git a/arrow/factory.py b/arrow/factory.py index 8cca6ac9b..aed4815c1 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -10,7 +10,8 @@ import calendar import warnings -from datetime import date, datetime, tzinfo +from datetime import date, datetime +from datetime import tzinfo as dt_tzinfo from time import struct_time from dateutil import tz as dateutil_tz @@ -163,8 +164,13 @@ def get(self, *args, **kwargs): # () -> now, @ utc. if arg_count == 0: - if isinstance(tz, tzinfo): + if isstr(tz): + tz = parser.TzinfoParser.parse(tz) return self.type.now(tz) + + if isinstance(tz, dt_tzinfo): + return self.type.now(tz) + return self.type.utcnow() if arg_count == 1: @@ -191,7 +197,7 @@ def get(self, *args, **kwargs): return self.type.fromdate(arg) # (tzinfo) -> now, @ tzinfo. - elif isinstance(arg, tzinfo): + elif isinstance(arg, dt_tzinfo): return self.type.now(arg) # (str) -> parse. @@ -220,7 +226,7 @@ def get(self, *args, **kwargs): if isinstance(arg_1, datetime): # (datetime, tzinfo/str) -> fromdatetime replace tzinfo. - if isinstance(arg_2, tzinfo) or isstr(arg_2): + if isinstance(arg_2, dt_tzinfo) or isstr(arg_2): return self.type.fromdatetime(arg_1, arg_2) else: raise TypeError( @@ -232,7 +238,7 @@ def get(self, *args, **kwargs): elif isinstance(arg_1, date): # (date, tzinfo/str) -> fromdate replace tzinfo. - if isinstance(arg_2, tzinfo) or isstr(arg_2): + if isinstance(arg_2, dt_tzinfo) or isstr(arg_2): return self.type.fromdate(arg_1, tzinfo=arg_2) else: raise TypeError( @@ -298,7 +304,7 @@ def now(self, tz=None): if tz is None: tz = dateutil_tz.tzlocal() - elif not isinstance(tz, tzinfo): + elif not isinstance(tz, dt_tzinfo): tz = parser.TzinfoParser.parse(tz) return self.type.now(tz) diff --git a/requirements.txt b/requirements.txt index ac54277ce..f5aa6daf9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,9 +2,9 @@ backports.functools_lru_cache==1.5.0 chai==1.1.2 nose==1.3.7 nose-cov==1.6 -pre-commit==1.17.0 -python-dateutil==2.8.0 -pytz==2019.1 -simplejson==3.16.0 -sphinx==1.8.5; python_version == '2.7' -sphinx==2.1.2; python_version >= '3.5' +pre-commit==1.18.* +python-dateutil==2.8.* +pytz==2019.* +simplejson==3.16.* +sphinx==1.8.*; python_version == '2.7' +sphinx==2.1.*; python_version >= '3.5' diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index cd3578655..87cfe6a76 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -8,6 +8,7 @@ import warnings from datetime import date, datetime, timedelta +import pytz import simplejson as json from chai import Chai from dateutil import tz @@ -22,12 +23,58 @@ def assertDtEqual(dt1, dt2, within=10): class ArrowInitTests(Chai): + def test_init_bad_input(self): + + with self.assertRaises(TypeError): + arrow.Arrow(2013) + + with self.assertRaises(TypeError): + arrow.Arrow(2013, 2) + + with self.assertRaises(ValueError): + 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()) + self.assertEqual(result._datetime, self.expected) + + result = arrow.Arrow(2013, 2, 2, 12) + self.expected = datetime(2013, 2, 2, 12, tzinfo=tz.tzutc()) + self.assertEqual(result._datetime, self.expected) + + result = arrow.Arrow(2013, 2, 2, 12, 30) + self.expected = datetime(2013, 2, 2, 12, 30, tzinfo=tz.tzutc()) + self.assertEqual(result._datetime, self.expected) + + result = arrow.Arrow(2013, 2, 2, 12, 30, 45) + self.expected = datetime(2013, 2, 2, 12, 30, 45, tzinfo=tz.tzutc()) + self.assertEqual(result._datetime, self.expected) + result = arrow.Arrow(2013, 2, 2, 12, 30, 45, 999999) self.expected = datetime(2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.tzutc()) + self.assertEqual(result._datetime, self.expected) + result = arrow.Arrow( + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") + ) + self.expected = datetime( + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") + ) + self.assertEqual(result._datetime, self.expected) + + # 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") + ) + self.expected = datetime( + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") + ) self.assertEqual(result._datetime, self.expected) + assertDtEqual(result._datetime, self.expected, 1) class ArrowFactoryTests(Chai): @@ -48,9 +95,14 @@ def test_fromtimestamp(self): timestamp = time.time() result = arrow.Arrow.fromtimestamp(timestamp) - assertDtEqual(result._datetime, datetime.now().replace(tzinfo=tz.tzlocal())) + result = arrow.Arrow.fromtimestamp(timestamp, tzinfo=tz.gettz("Europe/Paris")) + assertDtEqual( + result._datetime, + datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), + ) + def test_fromdatetime(self): dt = datetime(2013, 2, 3, 12, 30, 45, 1) @@ -90,11 +142,18 @@ 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") - self.assertEqual( 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") + ) + self.assertEqual( + result._datetime, + datetime(2013, 2, 3, 12, 30, 45, tzinfo=tz.gettz("Europe/Paris")), + ) + class ArrowRepresentationTests(Chai): def setUp(self): diff --git a/tests/factory_tests.py b/tests/factory_tests.py index acd4c36a0..3d75ffdc2 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -30,7 +30,7 @@ def test_timestamp_one_arg_no_arg(self): self.assertEqual(no_arg, one_arg) - def test_one_arg_non(self): + def test_one_arg_none(self): assertDtEqual( self.factory.get(None), datetime.utcnow().replace(tzinfo=tz.tzutc()) @@ -103,6 +103,16 @@ def test_kwarg_tzinfo(self): assertDtEqual(self.factory.get(tzinfo=tz.gettz("US/Pacific")), self.expected) + def test_kwarg_tzinfo_string(self): + + self.expected = ( + datetime.utcnow() + .replace(tzinfo=tz.tzutc()) + .astimezone(tz.gettz("US/Pacific")) + ) + + assertDtEqual(self.factory.get(tzinfo="US/Pacific"), self.expected) + def test_one_arg_iso_str(self): dt = datetime.utcnow() From d5d7b3561eed17f1a854e33983e9931ca669f013 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 19 Aug 2019 13:37:25 +0300 Subject: [PATCH 266/649] Remove README.rst.new file --- .gitignore | 2 ++ README.rst.new | 0 2 files changed, 2 insertions(+) delete mode 100644 README.rst.new diff --git a/.gitignore b/.gitignore index 723b75764..c68a2fbd9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +README.rst.new + # Small entry point file for debugging tasks test.py diff --git a/README.rst.new b/README.rst.new deleted file mode 100644 index e69de29bb..000000000 From 66d09f85de6491c0b38023d4d11de18c6a6420fb Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 19 Aug 2019 16:55:24 +0300 Subject: [PATCH 267/649] Reverted version increment; added tests to get coverage to 100% --- arrow/_version.py | 2 +- arrow/parser.py | 10 ++-------- tests/parser_tests.py | 25 +++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/arrow/_version.py b/arrow/_version.py index 9da2f8fcc..141826d55 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.15.0" +__version__ = "0.14.5" diff --git a/arrow/parser.py b/arrow/parser.py index ea3f6c6f4..3ce85f225 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -173,7 +173,6 @@ def parse_iso(self, datetime_string): ) has_tz = len(time_parts) == 2 - has_hours = hours is not None has_minutes = minutes is not None has_seconds = seconds is not None has_subseconds = subseconds is not None @@ -188,12 +187,8 @@ def parse_iso(self, datetime_string): time_string = "HH{time_sep}mm{time_sep}ss".format(time_sep=time_sep) elif has_minutes: time_string = "HH{time_sep}mm".format(time_sep=time_sep) - elif has_hours: - time_string = "HH" else: - raise ParserError( - "Invalid time component provided. Please specify a format or provide a valid time component in the basic or extended ISO 8601 time format." - ) + time_string = "HH" if has_space_divider: formats = ["{} {}".format(f, time_string) for f in formats] @@ -366,8 +361,7 @@ def _build_datetime(parts): timestamp = parts.get("timestamp") if timestamp is not None: - tz_utc = tz.tzutc() - return datetime.fromtimestamp(timestamp, tz=tz_utc) + return datetime.fromtimestamp(timestamp, tz=tz.tzutc()) day_of_year = parts.get("day_of_year") diff --git a/tests/parser_tests.py b/tests/parser_tests.py index bd13b6eb0..7381384c7 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -212,6 +212,18 @@ def test_parse_timestamp(self): self.parser.parse("{:f}".format(float_timestamp), "X"), self.expected ) + # test handling of ns timestamp (arrow will round to 6 digits regardless) + self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:f}123".format(float_timestamp), "X"), self.expected + ) + + # test ps timestamp (arrow will round to 6 digits regardless) + self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:f}123456".format(float_timestamp), "X"), self.expected + ) + # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will # break cases like "15 Jul 2000" and a format list (see issue #447) with self.assertRaises(ParserError): @@ -743,6 +755,19 @@ def test_YYYY_MM_DDTHH_mm(self): self.parser.parse_iso("2013-02-03T04:05"), datetime(2013, 2, 3, 4, 5) ) + def test_YYYY_MM_DDTHH(self): + + self.assertEqual( + self.parser.parse_iso("2013-02-03T04"), datetime(2013, 2, 3, 4) + ) + + def test_YYYY_MM_DDTHHZ(self): + + self.assertEqual( + 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): self.assertEqual( From 32fe0a57214b0852a7054050e95f6d186b521e4c Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 19 Aug 2019 17:47:37 +0300 Subject: [PATCH 268/649] Removed mixing of formatS --- tests/parser_tests.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 7381384c7..bc6b0e6ac 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -1034,22 +1034,6 @@ def test_iso8601_basic_format(self): datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), ) - # mixing formats--this may raise a ParserError in the future - self.assertEqual( - self.parser.parse_iso("2018-05-17T105513-0700"), - datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), - ) - - self.assertEqual( - self.parser.parse_iso("20180517T10:55:13-07:00"), - datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), - ) - - self.assertEqual( - self.parser.parse_iso("20180517T105513-07:00"), - datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), - ) - # ordinal in basic format: YYYYDDDD self.assertEqual(self.parser.parse_iso("1998136"), datetime(1998, 5, 16)) From 046cebfcb911087e72557f65ce895484a297a7a5 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 19 Aug 2019 20:13:53 +0300 Subject: [PATCH 269/649] Changed HISTORY.md to CHANGELOG.md and updated formatting to conform with Keep a Changelog standards (#645) --- HISTORY.md => CHANGELOG.md | 77 ++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 40 deletions(-) rename HISTORY.md => CHANGELOG.md (95%) diff --git a/HISTORY.md b/CHANGELOG.md similarity index 95% rename from HISTORY.md rename to CHANGELOG.md index 8cbcacccd..edab726f5 100644 --- a/HISTORY.md +++ b/CHANGELOG.md @@ -1,34 +1,34 @@ -## History +# Changelog -### 0.14.5 +## 0.14.5 - [NEW] Added Afrikaans locale. - [CHANGE] Removed deprecated `replace` shift functionality. Users looking to pass plural properties to the `replace` function to shift values should use `shift` instead. - [FIX] Fixed bug that occurred when `factory.get()` was passed a locale kwarg. -### 0.14.4 +## 0.14.4 - [FIX] Fixed a regression in 0.14.3 that prevented a tzinfo argument of type string to be passed to the `get()` function. Functionality such as `arrow.get("2019072807", "YYYYMMDDHH", tzinfo="UTC")` should work as normal again. - [CHANGE] Moved `backports.functools_lru_cache` dependency from `extra_requires` to `install_requires` for `Python 2.7` installs to fix [#495](https://github.com/crsmithdev/arrow/issues/495). -### 0.14.3 +## 0.14.3 - [NEW] Added full support for Python 3.8. -- [CHANGE] Added warnings for upcoming factory.get() parsing changes in 0.15.0. Please see https://github.com/crsmithdev/arrow/issues/612 for full details. +- [CHANGE] Added warnings for upcoming factory.get() parsing changes in 0.15.0. Please see [issue #612](https://github.com/crsmithdev/arrow/issues/612) for full details. - [FIX] Extensive refactor and update of documentation. - [FIX] factory.get() can now construct from kwargs. - [FIX] Added meridians to Spanish Locale. -### 0.14.2 +## 0.14.2 - [CHANGE] Travis CI builds now use tox to lint and run tests. - [FIX] Fixed UnicodeDecodeError on certain locales (#600). -### 0.14.1 +## 0.14.1 - [FIX] Fixed "ImportError: No module named 'dateutil'" (#598). -### 0.14.0 +## 0.14.0 - [NEW] Added provisional support for Python 3.8. - [CHANGE] Removed support for EOL Python 3.4. @@ -37,7 +37,7 @@ - [FIX] Enabled flake8 and black on travis builds. - [FIX] Formatted code using black and isort. -### 0.13.2 +## 0.13.2 - [NEW] Add is_between method. - [FIX] Improved humanize behaviour for near zero durations (#416). @@ -45,13 +45,13 @@ - [FIX] Documentation updates. - [FIX] Improvements to German Locale. -### 0.13.1 +## 0.13.1 - [NEW] Add support for Python 3.7. - [CHANGE] Remove deprecation decorators for Arrow.range(), Arrow.span_range() and Arrow.interval(), all now return generators, wrap with list() to get old behavior. - [FIX] Documentation and docstring updates. -### 0.13.0 +## 0.13.0 - [NEW] Added support for Python 3.6. - [CHANGE] Drop support for Python 2.6/3.3. @@ -68,16 +68,16 @@ - [FIX] Improve docs for get, now and utcnow methods. - [FIX] Correct typo in depreciation warning. -### 0.12.1 +## 0.12.1 - [FIX] Allow universal wheels to be generated and reliably installed. - [FIX] Make humanize respect only_distance when granularity argument is also given. -### 0.12.0 +## 0.12.0 - [FIX] Compatibility fix for Python 2.x -### 0.11.0 +## 0.11.0 - [FIX] Fix grammar of ArabicLocale - [NEW] Add Nepali Locale @@ -87,13 +87,13 @@ - [FIX] Remove pip --user-mirrors flag - [NEW] Add Indonesian Locale -### 0.10.0 +## 0.10.0 - [FIX] Fix getattr off by one for quarter - [FIX] Fix negative offset for UTC - [FIX] Update arrow.py -### 0.9.0 +## 0.9.0 - [NEW] Remove duplicate code - [NEW] Support gnu date iso 8601 @@ -108,16 +108,16 @@ - [NEW] Azerbaijani locale added, locale issue fixed in Turkish. - [FIX] Format ParserError's raise message -### 0.8.0 +## 0.8.0 - [] -### 0.7.1 +## 0.7.1 - [NEW] Esperanto locale (batisteo) +## 0.7.0 -### 0.7.0 - [FIX] Parse localized strings #228 (swistakm) - [FIX] Modify tzinfo parameter in `get` api #221 (bottleimp) - [FIX] Fix Czech locale (PrehistoricTeam) @@ -130,7 +130,7 @@ - [NEW] Search date in strings (beenje) - [NEW] Note that arrow's tokens differ from strptime's. (offby1) -### 0.6.0 +## 0.6.0 - [FIX] Added support for Python 3 - [FIX] Avoid truncating oversized epoch timestamps. Fixes #216. @@ -147,8 +147,7 @@ - [NEW] Add count argument to span method - [NEW] Improved docs - -### 0.5.1 - 0.5.4 +## 0.5.1 - 0.5.4 - [FIX] test the behavior of simplejson instead of calling for_json directly (tonyseek) - [FIX] Add Hebrew Locale (doodyparizada) @@ -156,7 +155,7 @@ - [FIX] Update setup.py Development Status level (andrewelkins) - [FIX] Case insensitive month match (cshowe) -### 0.5.0 +## 0.5.0 - [NEW] struct_time addition. (mhworth) - [NEW] Version grep (eirnym) @@ -170,7 +169,7 @@ - [FIX] Parse lower-cased 'h' (tamentis) - [FIX] Slight modifications to Dutch locale (nvie) -### 0.4.4 +## 0.4.4 - [NEW] Include the docs in the released tarball - [NEW] Czech localization Czech localization for Arrow @@ -188,8 +187,7 @@ - [FIX] In Norwegian and new Norwegian months and weekdays should not be capitalized - [FIX] Fixed discrepancy between specifying 'X' to arrow.get and specifying no template - -### 0.4.3 +## 0.4.3 - [NEW] Turkish locale (Emre) - [NEW] Arabic locale (Mosab Ahmad) @@ -210,8 +208,7 @@ - [FIX] Error messages for parse errors are now more descriptive (Maciej Albin) - [FIX] The parser now correctly checks for separators in strings (Mschwager) - -### 0.4.2 +## 0.4.2 - [NEW] Factory ``get`` method now accepts a single ``Arrow`` argument. - [NEW] Tokens SSSS, SSSSS and SSSSSS are supported in parsing. @@ -225,7 +222,7 @@ - [FIX] ``humanize`` now correctly returns unicode (Shvechikov) - [FIX] ``Arrow`` objects now pickle / unpickle correctly (Yoloseem) -### 0.4.1 +## 0.4.1 - [NEW] Table / explanation of formatting & parsing tokens in docs - [NEW] Brazilian locale (Augusto2112) @@ -237,7 +234,7 @@ - [FIX] Factory ``get`` method should now handle unicode strings correctly (Bwells) - [FIX] Midnight and noon should now parse and format correctly (Bwells) -### 0.4.0 +## 0.4.0 - [NEW] Format-free ISO-8601 parsing in factory ``get`` method - [NEW] Support for 'week' / 'weeks' in ``span``, ``range``, ``span_range``, ``floor`` and ``ceil`` @@ -252,18 +249,18 @@ - [CHANGE] Dropped timestamp support in ``range`` and ``span_range`` (never worked correctly) - [CHANGE] Dropped parsing of single string as tz string in factory ``get`` method (replaced by ISO-8601) -### 0.3.5 +## 0.3.5 - [NEW] French locale (Cynddl) - [NEW] Spanish locale (Slapresta) - [FIX] Ranges handle multiple timezones correctly (Ftobia) -### 0.3.4 +## 0.3.4 - [FIX] Humanize no longer sometimes returns the wrong month delta - [FIX] ``__format__`` works correctly with no format string -### 0.3.3 +## 0.3.3 - [NEW] Python 2.6 support - [NEW] Initial support for locale-based parsing and formatting @@ -271,15 +268,15 @@ - [NEW] ``factory`` api method to obtain a factory for a custom type - [FIX] Python 3 support and tests completely ironed out -### 0.3.2 +## 0.3.2 - [NEW] Python 3+ support -### 0.3.1 +## 0.3.1 - [FIX] The old ``arrow`` module function handles timestamps correctly as it used to -### 0.3.0 +## 0.3.0 - [NEW] ``Arrow.replace`` method - [NEW] Accept timestamps, datetimes and Arrows for datetime inputs, where reasonable @@ -288,12 +285,12 @@ - [CHANGE] Plural attribute name semantics altered: single -> absolute, plural -> relative - [CHANGE] Plural names no longer supported as properties (e.g. ``arrow.utcnow().years``) -### 0.2.1 +## 0.2.1 - [NEW] Support for localized humanization - [NEW] English, Russian, Greek, Korean, Chinese locales -### 0.2.0 +## 0.2.0 - **REWRITE** - [NEW] Date parsing @@ -303,14 +300,14 @@ - [NEW] ``clone`` method - [NEW] ``get``, ``now`` and ``utcnow`` API methods -### 0.1.6 +## 0.1.6 - [NEW] Humanized time deltas - [NEW] ``__eq__`` implemented - [FIX] Issues with conversions related to daylight savings time resolved - [CHANGE] ``__str__`` uses ISO formatting -### 0.1.5 +## 0.1.5 - **Started tracking changes** - [NEW] Parsing of ISO-formatted time zone offsets (e.g. '+02:30', '-05:00') From fce69267f6d25f2bf726d97a5ab0a8ec64762b10 Mon Sep 17 00:00:00 2001 From: Matthew Dooler Date: Mon, 19 Aug 2019 18:21:00 +0100 Subject: [PATCH 270/649] Handle date with over 999999 microseconds (#643) * Handle date with over 999999 microseconds * Fix formatting * Tests and comments --- arrow/parser.py | 33 +++++++++++++++++++++++---------- tests/parser_tests.py | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 7266ff957..f82d3acd5 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals import re -from datetime import datetime +from datetime import datetime, timedelta from dateutil import tz @@ -275,15 +275,28 @@ def _build_datetime(parts): elif am_pm == "am" and hour == 12: hour = 0 - return datetime( - year=parts.get("year", 1), - month=parts.get("month", 1), - day=parts.get("day", 1), - hour=hour, - minute=parts.get("minute", 0), - second=parts.get("second", 0), - microsecond=parts.get("microsecond", 0), - tzinfo=parts.get("tzinfo"), + # account for rounding up to 1000000 + microsecond = parts.get("microsecond", 0) + if microsecond == 1000000: + microsecond = 0 + second_increment = 1 + else: + second_increment = 0 + + increment = timedelta(seconds=second_increment) + + return ( + datetime( + year=parts.get("year", 1), + month=parts.get("month", 1), + day=parts.get("day", 1), + hour=hour, + minute=parts.get("minute", 0), + second=parts.get("second", 0), + microsecond=microsecond, + tzinfo=parts.get("tzinfo"), + ) + + increment ) def _parse_multiformat(self, string, formats): diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 0ecd13a62..095fa5040 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -315,6 +315,29 @@ def test_parse_subsecond_rounding(self): self.assertEqual(self.parser.parse(string, format), self.expected) self.assertEqual(self.parser.parse_iso(string), self.expected) + # overflow (zero out the subseconds and increment the seconds) + # regression tests for issue #636 + self.expected = datetime(2013, 1, 1, 12, 30, 46) + string = "2013-01-01 12:30:45.9999995" + self.assertEqual(self.parser.parse(string, format), self.expected) + self.assertEqual(self.parser.parse_iso(string), self.expected) + + self.expected = datetime(2013, 1, 1, 12, 31, 0) + string = "2013-01-01 12:30:59.9999999" + self.assertEqual(self.parser.parse(string, format), self.expected) + self.assertEqual(self.parser.parse_iso(string), self.expected) + + self.expected = datetime(2013, 1, 2, 0, 0, 0) + string = "2013-01-01 23:59:59.9999999" + self.assertEqual(self.parser.parse(string, format), self.expected) + self.assertEqual(self.parser.parse_iso(string), self.expected) + + # 6 digits should remain unrounded + self.expected = datetime(2013, 1, 1, 12, 30, 45, 999999) + string = "2013-01-01 12:30:45.999999" + self.assertEqual(self.parser.parse(string, format), self.expected) + self.assertEqual(self.parser.parse_iso(string), self.expected) + def test_map_lookup_keyerror(self): with self.assertRaises(parser.ParserError): From a89c5312bfa4d5f8c23229c717cc19214a79c908 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 20 Aug 2019 21:10:31 +0300 Subject: [PATCH 271/649] Add week granulation and week locale handling (#642) * Added week granulation and week locale handling. * Adding more tests for week granularity. --- arrow/arrow.py | 10 +++++++++- arrow/locales.py | 4 ++++ tests/arrow_tests.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 3a6f2ef8b..0bd59364b 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -888,10 +888,16 @@ def humanize( elif diff < 129600: return locale.describe("day", sign, only_distance=only_distance) - elif diff < 2160000: + elif diff < 554400: days = sign * int(max(delta / 86400, 2)) return locale.describe("days", days, only_distance=only_distance) + elif diff < 907200: + return locale.describe("week", sign, only_distance=only_distance) + elif diff < 2419200: + weeks = sign * int(max(delta / 604800, 2)) + return locale.describe("weeks", weeks, only_distance=only_distance) + elif diff < 3888000: return locale.describe("month", sign, only_distance=only_distance) elif diff < 29808000: @@ -919,6 +925,8 @@ def humanize( delta = sign * delta / float(60 * 60) elif granularity == "day": delta = sign * delta / float(60 * 60 * 24) + elif granularity == "week": + delta = sign * delta / float(60 * 60 * 24 * 7) elif granularity == "month": delta = sign * delta / float(60 * 60 * 24 * 30.5) elif granularity == "year": diff --git a/arrow/locales.py b/arrow/locales.py index 4e540f0bf..1d4b890f1 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -39,6 +39,8 @@ class Locale(object): "hours": "", "day": "", "days": "", + "week": "", + "weeks": "", "month": "", "months": "", "year": "", @@ -208,6 +210,8 @@ class EnglishLocale(Locale): "hours": "{0} hours", "day": "a day", "days": "{0} days", + "week": "a week", + "weeks": "{0} weeks", "month": "a month", "months": "{0} months", "year": "a year", diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 87cfe6a76..9c79c7edd 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1409,6 +1409,8 @@ def test_granularity(self): self.assertEqual(later105.humanize(self.now, granularity="hour"), "in 27 hours") self.assertEqual(self.now.humanize(later105, granularity="day"), "a day ago") self.assertEqual(later105.humanize(self.now, granularity="day"), "in a day") + self.assertEqual(self.now.humanize(later105, granularity="week"), "0 weeks ago") + self.assertEqual(later105.humanize(self.now, granularity="week"), "in 0 weeks") self.assertEqual( self.now.humanize(later105, granularity="month"), "0 months ago" ) @@ -1419,6 +1421,8 @@ def test_granularity(self): later106 = self.now.shift(seconds=3 * 10 ** 6) self.assertEqual(self.now.humanize(later106, granularity="day"), "34 days ago") self.assertEqual(later106.humanize(self.now, granularity="day"), "in 34 days") + self.assertEqual(self.now.humanize(later106, granularity="week"), "4 weeks ago") + self.assertEqual(later106.humanize(self.now, granularity="week"), "in 4 weeks") self.assertEqual( self.now.humanize(later106, granularity="month"), "a month ago" ) @@ -1427,6 +1431,10 @@ def test_granularity(self): self.assertEqual(later106.humanize(self.now, granularity="year"), "in 0 years") later506 = self.now.shift(seconds=50 * 10 ** 6) + self.assertEqual( + self.now.humanize(later506, granularity="week"), "82 weeks ago" + ) + self.assertEqual(later506.humanize(self.now, granularity="week"), "in 82 weeks") self.assertEqual( self.now.humanize(later506, granularity="month"), "18 months ago" ) @@ -1536,6 +1544,26 @@ def test_days(self): later = self.now.shift(days=4) self.assertEqual(later.humanize(), "in 4 days") + def test_week(self): + + later = self.now.shift(weeks=1) + + self.assertEqual(self.now.humanize(later), "a week ago") + self.assertEqual(later.humanize(self.now), "in a week") + + self.assertEqual(self.now.humanize(later, only_distance=True), "a week") + self.assertEqual(later.humanize(self.now, only_distance=True), "a week") + + def test_weeks(self): + + later = self.now.shift(weeks=2) + + self.assertEqual(self.now.humanize(later), "2 weeks ago") + self.assertEqual(later.humanize(self.now), "in 2 weeks") + + self.assertEqual(self.now.humanize(later, only_distance=True), "2 weeks") + self.assertEqual(later.humanize(self.now, only_distance=True), "2 weeks") + def test_month(self): later = self.now.shift(months=1) From 4e2bd2b9d638cfe842af236bd7b3fb6e19714a72 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 24 Aug 2019 14:16:21 +0300 Subject: [PATCH 272/649] Updated docs, revamped contributing section, added doc linting (#647) * Cleaned up docs and improved contributing section * Added linting for docs and updated Travis config to use Ubuntu 18 * Cleaned up Makefile; added groovy emoji * Adjusted emoji * Removed emoji * Tweaked wording in features section * Added emoji in contributing. * Fix Travis CI Python 3.5 build * Removed quieting of docs linting output to provide more info * Added emoji to docs title and minor tweaks to wording * Capitalize project title * Re-added make.bat and made project name a unicode string --- .travis.yml | 22 +++++++++------ Makefile | 5 +--- README.rst | 43 ++++++++++++++++------------- docs/conf.py | 4 +-- docs/make.bat | 70 ++++++++++++++++++++++++------------------------ requirements.txt | 2 +- tox.ini | 15 ++++++++++- 7 files changed, 91 insertions(+), 70 deletions(-) diff --git a/.travis.yml b/.travis.yml index 35a55a21b..7087e8f3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,26 @@ language: python -dist: xenial +dist: bionic matrix: include: - - python: 2.7 + - name: "Python 2.7" + python: "2.7" env: TOXENV=py27 - - python: 3.5 + - name: "Python 3.5" + dist: xenial + python: "3.5" env: TOXENV=py35 - - python: 3.6 + - name: "Python 3.6" + python: "3.6" env: TOXENV=py36 - - python: 3.7 + - name: "Python 3.7" + python: "3.7" env: TOXENV=py37 - - python: 3.8-dev + - name: "Python 3.8" + python: "3.8-dev" env: TOXENV=py38 - name: "Linting" - python: 3.7 - env: TOXENV=lint + python: "3.7" + env: TOXENV=lint,docs cache: directories: - $HOME/.cache/pre-commit diff --git a/Makefile b/Makefile index af4345e8d..e7d324d1a 100644 --- a/Makefile +++ b/Makefile @@ -17,14 +17,11 @@ test: rm -f .coverage . venv/bin/activate && nosetests -test-dev: - rm -f .coverage - . venv/bin/activate && python -Wd -m nose - lint: venv/bin/pre-commit run --all-files --show-diff-on-failure docs: + rm -rf docs/_build . venv/bin/activate && cd docs; make html clean: diff --git a/README.rst b/README.rst index 84eaa2982..0a9969242 100644 --- a/README.rst +++ b/README.rst @@ -25,39 +25,37 @@ Arrow: Better dates & times for Python .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :alt: Code Style: Black - :target: https://github.com/python/black + :target: https://github.com/psf/black -What? ------ -Arrow is a Python library that offers a sensible, human-friendly approach to creating, manipulating, formatting and converting dates, times, and timestamps. It implements and updates the datetime type, plugging gaps in functionality, and provides an intelligent module API that supports many common creation scenarios. Simply put, it helps you work with dates and times with fewer imports and a lot less code. +**Arrow** is a Python library that offers a sensible and human-friendly approach to creating, manipulating, formatting and converting dates, times and timestamps. It implements and updates the datetime type, plugging gaps in functionality and providing an intelligent module API that supports many common creation scenarios. Simply put, it helps you work with dates and times with fewer imports and a lot less code. -Arrow is heavily inspired by `moment.js `_ and `requests `_. +Arrow is named after the `arrow of time `_ and is heavily inspired by `moment.js `_ and `requests `_. -Why? ----- +Why use Arrow over built-in modules? +------------------------------------ -Python's standard library and some other low-level modules have near-complete date, time and timezone functionality but don't work very well from a usability perspective: +Python's standard library and some other low-level modules have near-complete date, time and timezone functionality, but don't work very well from a usability perspective: -- Too many modules: datetime, time, calendar, dateutil, pytz and more -- Too many types: date, time, datetime, tzinfo, timedelta, relativedelta, etc. +- Too many modules: datetime, time, calendar, dateutil, pytz and more +- Too many types: date, time, datetime, tzinfo, timedelta, relativedelta, etc. - Timezones and timestamp conversions are verbose and unpleasant - Timezone naivety is the norm -- Gaps in functionality: ISO-8601 parsing, timespans, humanization +- Gaps in functionality: ISO-8601 parsing, timespans, humanization Features -------- -- Fully implemented, drop-in replacement for datetime +- Fully-implemented, drop-in replacement for datetime - Supports Python 2.7, 3.5, 3.6, 3.7 and 3.8 -- Timezone-aware & UTC by default +- Timezone-aware and UTC by default - Provides super-simple creation options for many common input scenarios - :code:`shift` method with support for relative offsets, including weeks - Formats and parses strings automatically -- Partial support for ISO-8601 +- Wide support for ISO-8601 - Timezone conversion - Timestamp available as a property -- Generates time spans, ranges, floors and ceilings in timeframes from year to microsecond +- Generates time spans, ranges, floors and ceilings for time frames ranging from microsecond to year - Humanizes and supports a growing list of contributed locales - Extensible for your own Arrow-derived types @@ -79,6 +77,9 @@ Example Usage .. code-block:: python >>> import arrow + >>> arrow.get('2013-05-11T21:23:58.970460+07:00') + + >>> utc = arrow.utcnow() >>> utc @@ -91,9 +92,6 @@ Example Usage >>> local - >>> arrow.get('2013-05-11T21:23:58.970460+00:00') - - >>> local.timestamp 1368303838 @@ -119,4 +117,11 @@ For full documentation, please visit `arrow.readthedocs.io `_ and then fork `this repository `_ on GitHub to begin making changes. If you would like to help with localizations, please see `locales.py `_ to see what locales are currently supported. If you are helping with code, make sure to add a few tests to ensure that the bug was fixed or the feature works as intended. +Contributions are welcome for both code and localizations (adding and updating locales). Begin by gaining familiarity with the Arrow library and its features. Then, jump into contributing: + +1. Find an issue or feature to tackle on the `issue tracker `_. Issues marked with the `"good first issue" label `_ may be a great place to start! +2. Fork `this repository `_ on GitHub and begin making changes in a branch. +3. Add a few tests to ensure that the bug was fixed or the feature works as expected. +4. Submit a pull request and await feedback 😃. + +If you have any questions along the way, feel free to ask them `here `_. diff --git a/docs/conf.py b/docs/conf.py index 1e67b23c3..9ae9b07b1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,7 +14,7 @@ # -- Project information ----------------------------------------------------- -project = "arrow" +project = u"Arrow 🏹" copyright = "2019, Chris Smith" author = "Chris Smith" @@ -45,7 +45,7 @@ html_show_copyright = True html_theme_options = { - "description": "Arrow is a sensible and human-friendly approach to dates, times, and timestamps.", + "description": "Arrow is a sensible and human-friendly approach to dates, times and timestamps.", "github_user": "crsmithdev", "github_repo": "arrow", "github_banner": True, diff --git a/docs/make.bat b/docs/make.bat index 2119f5109..922152e96 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,35 +1,35 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/requirements.txt b/requirements.txt index f5aa6daf9..54eda346b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,4 @@ python-dateutil==2.8.* pytz==2019.* simplejson==3.16.* sphinx==1.8.*; python_version == '2.7' -sphinx==2.1.*; python_version >= '3.5' +sphinx==2.2.*; python_version >= '3.5' diff --git a/tox.ini b/tox.ini index 8fb5ab89b..061f39261 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{27,35,36,37,38},lint +envlist = py{27,35,36,37,38},lint,docs skip_missing_interpreters = true [testenv] @@ -14,3 +14,16 @@ deps = pre-commit commands = pre-commit install pre-commit run --all-files --show-diff-on-failure + +[testenv:docs] +basepython = python3 +skip_install = true +changedir = docs +deps = + doc8 + sphinx + python-dateutil +whitelist_externals = make +commands = + doc8 index.rst ../README.rst --extension .rst --ignore D001 + make html SPHINXOPTS="-W --keep-going" From 52354fe79f8f4eccd05dac76de343d2aaecee6ca Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 24 Aug 2019 15:39:24 +0300 Subject: [PATCH 273/649] Added locale validation and translation request on untranslated timeframes (#646) * Added week granulation and week locale handling. * Adding more tests for week granularity. * added validation test and handled key error for new week granularity * Commented choice_re function * Increased test coverage to 100% * Added mention of week granularity to humanize docstring * Added test for invalid string tzinfo to get() * Made meridians a tuple instead of a list * Added validation of list strings to validator --- arrow/arrow.py | 160 ++++++++++++++++++++++------------------- arrow/locales.py | 8 +-- arrow/parser.py | 19 +++-- requirements.txt | 1 + setup.cfg | 2 +- tests/arrow_tests.py | 17 +++++ tests/factory_tests.py | 4 ++ tests/locales_tests.py | 35 +++++++++ 8 files changed, 160 insertions(+), 86 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 0bd59364b..fa84b22a9 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -829,7 +829,7 @@ def humanize( Defaults to now in the current :class:`Arrow ` object's timezone. :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'. :param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part. - :param granularity: (optional) defines the precision of the output. Set it to strings 'second', 'minute', 'hour', 'day', 'month' or 'year'. + :param granularity: (optional) defines the precision of the output. Set it to strings 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'. Usage:: @@ -843,6 +843,7 @@ def humanize( """ + locale_name = locale locale = locales.get_locale(locale) if other is None: @@ -866,80 +867,92 @@ def humanize( diff = abs(delta) delta = diff - if granularity == "auto": - if diff < 10: - return locale.describe("now", only_distance=only_distance) - - if diff < 45: - seconds = sign * delta - return locale.describe("seconds", seconds, only_distance=only_distance) - - elif diff < 90: - return locale.describe("minute", sign, only_distance=only_distance) - elif diff < 2700: - minutes = sign * int(max(delta / 60, 2)) - return locale.describe("minutes", minutes, only_distance=only_distance) - - elif diff < 5400: - return locale.describe("hour", sign, only_distance=only_distance) - elif diff < 79200: - hours = sign * int(max(delta / 3600, 2)) - return locale.describe("hours", hours, only_distance=only_distance) - - elif diff < 129600: - return locale.describe("day", sign, only_distance=only_distance) - elif diff < 554400: - days = sign * int(max(delta / 86400, 2)) - return locale.describe("days", days, only_distance=only_distance) - - elif diff < 907200: - return locale.describe("week", sign, only_distance=only_distance) - elif diff < 2419200: - weeks = sign * int(max(delta / 604800, 2)) - return locale.describe("weeks", weeks, only_distance=only_distance) - - elif diff < 3888000: - return locale.describe("month", sign, only_distance=only_distance) - elif diff < 29808000: - self_months = self._datetime.year * 12 + self._datetime.month - other_months = dt.year * 12 + dt.month - - months = sign * int(max(abs(other_months - self_months), 2)) - - return locale.describe("months", months, only_distance=only_distance) - - elif diff < 47260800: - return locale.describe("year", sign, only_distance=only_distance) - else: - years = sign * int(max(delta / 31536000, 2)) - return locale.describe("years", years, only_distance=only_distance) - - else: - if granularity == "second": - delta = sign * delta - if abs(delta) < 2: + try: + if granularity == "auto": + if diff < 10: return locale.describe("now", only_distance=only_distance) - elif granularity == "minute": - delta = sign * delta / float(60) - elif granularity == "hour": - delta = sign * delta / float(60 * 60) - elif granularity == "day": - delta = sign * delta / float(60 * 60 * 24) - elif granularity == "week": - delta = sign * delta / float(60 * 60 * 24 * 7) - elif granularity == "month": - delta = sign * delta / float(60 * 60 * 24 * 30.5) - elif granularity == "year": - delta = sign * delta / float(60 * 60 * 24 * 365.25) + + if diff < 45: + seconds = sign * delta + return locale.describe( + "seconds", seconds, only_distance=only_distance + ) + + elif diff < 90: + return locale.describe("minute", sign, only_distance=only_distance) + elif diff < 2700: + minutes = sign * int(max(delta / 60, 2)) + return locale.describe( + "minutes", minutes, only_distance=only_distance + ) + + elif diff < 5400: + return locale.describe("hour", sign, only_distance=only_distance) + elif diff < 79200: + hours = sign * int(max(delta / 3600, 2)) + return locale.describe("hours", hours, only_distance=only_distance) + + elif diff < 129600: + return locale.describe("day", sign, only_distance=only_distance) + elif diff < 554400: + days = sign * int(max(delta / 86400, 2)) + return locale.describe("days", days, only_distance=only_distance) + + elif diff < 907200: + return locale.describe("week", sign, only_distance=only_distance) + elif diff < 2419200: + weeks = sign * int(max(delta / 604800, 2)) + return locale.describe("weeks", weeks, only_distance=only_distance) + + elif diff < 3888000: + return locale.describe("month", sign, only_distance=only_distance) + elif diff < 29808000: + self_months = self._datetime.year * 12 + self._datetime.month + other_months = dt.year * 12 + dt.month + + months = sign * int(max(abs(other_months - self_months), 2)) + + return locale.describe( + "months", months, only_distance=only_distance + ) + + elif diff < 47260800: + return locale.describe("year", sign, only_distance=only_distance) + else: + years = sign * int(max(delta / 31536000, 2)) + return locale.describe("years", years, only_distance=only_distance) + else: - raise AttributeError( - 'Error. Could not understand your level of granularity. Please select between \ - "second", "minute", "hour", "day", "week", "month" or "year"' + if granularity == "second": + delta = sign * delta + if abs(delta) < 2: + return locale.describe("now", only_distance=only_distance) + elif granularity == "minute": + delta = sign * delta / float(60) + elif granularity == "hour": + delta = sign * delta / float(60 * 60) + elif granularity == "day": + delta = sign * delta / float(60 * 60 * 24) + elif granularity == "week": + delta = sign * delta / float(60 * 60 * 24 * 7) + elif granularity == "month": + delta = sign * delta / float(60 * 60 * 24 * 30.5) + elif granularity == "year": + delta = sign * delta / float(60 * 60 * 24 * 365.25) + else: + raise AttributeError( + "Invalid level of granularity. Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'" + ) + + if trunc(abs(delta)) != 1: + granularity += "s" + return locale.describe(granularity, delta, only_distance=only_distance) + except KeyError as e: + raise ValueError( + "Humanization of the {} granularity is not currently translated in the '{}' locale. Please consider making a contribution to this locale.".format( + e, locale_name ) - - if trunc(abs(delta)) != 1: - granularity += "s" - return locale.describe(granularity, delta, only_distance=only_distance) + ) # query functions @@ -975,8 +988,7 @@ def is_between(self, start, end, bounds="()"): if bounds != "()" and bounds != "(]" and bounds != "[)" and bounds != "[]": raise AttributeError( - 'Error. Could not understand the specified bounds. Please select between \ - "()", "(]", "[)", or "[]"' + 'Invalid bounds. Please select between "()", "(]", "[)", or "[]".' ) if not isinstance(start, Arrow): diff --git a/arrow/locales.py b/arrow/locales.py index 1d4b890f1..ae4bff71f 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1513,7 +1513,7 @@ class MacedonianLocale(SlavicBaseLocale): ] -class _DeutschLocaleCommonMixin(object): +class DeutschBaseLocale(Locale): past = "vor {0}" future = "in {0}" @@ -1605,12 +1605,12 @@ def describe(self, timeframe, delta=0, only_distance=False): return humanized -class GermanLocale(_DeutschLocaleCommonMixin, Locale): +class GermanLocale(DeutschBaseLocale, Locale): names = ["de", "de_de"] -class AustrianLocale(_DeutschLocaleCommonMixin, Locale): +class AustrianLocale(DeutschBaseLocale, Locale): names = ["de_at"] @@ -3072,7 +3072,7 @@ def _map_locales(): locales = {} for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): - if issubclass(cls, Locale): + if issubclass(cls, Locale): # pragma: no branch for name in cls.names: locales[name.lower()] = cls diff --git a/arrow/parser.py b/arrow/parser.py index f82d3acd5..9930ab08c 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -63,22 +63,26 @@ def __init__(self, locale="en_us", cache_size=0): self._input_re_map = self._BASE_INPUT_RE_MAP.copy() self._input_re_map.update( { - "MMMM": self._choice_re(self.locale.month_names[1:], re.IGNORECASE), - "MMM": self._choice_re( + "MMMM": self._generate_choice_re( + self.locale.month_names[1:], re.IGNORECASE + ), + "MMM": self._generate_choice_re( self.locale.month_abbreviations[1:], re.IGNORECASE ), "Do": re.compile(self.locale.ordinal_day_re), - "dddd": self._choice_re(self.locale.day_names[1:], re.IGNORECASE), - "ddd": self._choice_re( + "dddd": self._generate_choice_re( + self.locale.day_names[1:], re.IGNORECASE + ), + "ddd": self._generate_choice_re( self.locale.day_abbreviations[1:], re.IGNORECASE ), "d": re.compile(r"[1-7]"), - "a": self._choice_re( + "a": self._generate_choice_re( (self.locale.meridians["am"], self.locale.meridians["pm"]) ), # note: 'A' token accepts both 'am/pm' and 'AM/PM' formats to # ensure backwards compatibility of this token - "A": self._choice_re(self.locale.meridians.values()), + "A": self._generate_choice_re(self.locale.meridians.values()), } ) if cache_size > 0: @@ -333,8 +337,9 @@ def _try_timestamp(string): except Exception: return None + # generates a capture group of choices separated by an OR operator @staticmethod - def _choice_re(choices, flags=0): + def _generate_choice_re(choices, flags=0): return re.compile(r"({})".format("|".join(choices)), flags=flags) diff --git a/requirements.txt b/requirements.txt index 54eda346b..e3b1a1e9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ backports.functools_lru_cache==1.5.0 chai==1.1.2 +mock==3.0.* nose==1.3.7 nose-cov==1.6 pre-commit==1.18.* diff --git a/setup.cfg b/setup.cfg index fa26fd973..d93e3fc9b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ ignore = E203,E501,W503 line_length = 88 multi_line_output = 3 include_trailing_comma = true -known_third_party = chai,dateutil,pytz,setuptools,simplejson +known_third_party = chai,dateutil,mock,pytz,setuptools,simplejson [bdist_wheel] universal = 1 diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 9c79c7edd..5e1baf4fc 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -13,6 +13,7 @@ from chai import Chai from dateutil import tz from dateutil.relativedelta import FR, MO, SA, SU, TH, TU, WE +from mock import patch from arrow import arrow, util @@ -1649,6 +1650,22 @@ def test_none(self): self.assertEqual(result, "just now") + result = arw.humanize(None) + + self.assertEqual(result, "just now") + + def test_untranslated_granularity(self): + + arw = arrow.Arrow.utcnow() + later = arw.shift(weeks=1) + + # simulate an untranslated timeframe key + with patch.dict("arrow.locales.EnglishLocale.timeframes"): + del arrow.locales.EnglishLocale.timeframes["week"] + + with self.assertRaises(ValueError): + arw.humanize(later, granularity="week") + class ArrowHumanizeTestsWithLocale(Chai): def setUp(self): diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 3d75ffdc2..f2ec3add1 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -6,6 +6,7 @@ from dateutil import tz from arrow import factory, util +from arrow.parser import ParserError def assertDtEqual(dt1, dt2, within=10): @@ -113,6 +114,9 @@ def test_kwarg_tzinfo_string(self): assertDtEqual(self.factory.get(tzinfo="US/Pacific"), self.expected) + with self.assertRaises(ParserError): + self.factory.get(tzinfo="US/PacificInvalidTzinfo") + def test_one_arg_iso_str(self): dt = datetime.utcnow() diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 0abf2083d..ae68eea22 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -6,6 +6,41 @@ from arrow import arrow, locales +class LocaleValidationTests(Chai): + """Validate locales to ensure that translations are valid and complete""" + + def setUp(self): + super(LocaleValidationTests, self).setUp() + + self.locales = locales._locales + + def test_locale_validation(self): + + for _, locale_cls in self.locales.items(): + # 7 days + 1 spacer to allow for 1-indexing of months + self.assertEqual(len(locale_cls.day_names), 8) + self.assertTrue(locale_cls.day_names[0] == "") + # ensure that all string from index 1 onward are valid (not blank or None) + self.assertTrue(all(locale_cls.day_names[1:])) + + self.assertEqual(len(locale_cls.day_abbreviations), 8) + self.assertTrue(locale_cls.day_abbreviations[0] == "") + self.assertTrue(all(locale_cls.day_abbreviations[1:])) + + # 12 months + 1 spacer to allow for 1-indexing of months + self.assertEqual(len(locale_cls.month_names), 13) + self.assertTrue(locale_cls.month_names[0] == "") + self.assertTrue(all(locale_cls.month_names[1:])) + + self.assertEqual(len(locale_cls.month_abbreviations), 13) + self.assertTrue(locale_cls.month_abbreviations[0] == "") + self.assertTrue(all(locale_cls.month_abbreviations[1:])) + + self.assertTrue(len(locale_cls.names) > 0) + self.assertTrue(locale_cls.past is not None) + self.assertTrue(locale_cls.future is not None) + + class ModuleTests(Chai): def test_get_locale(self): From 0861bbe8fdae70a71152a71cdbff1da5e2ae8df8 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 27 Aug 2019 19:48:31 +0300 Subject: [PATCH 274/649] Made a number of tweaks to the MANIFEST and tweaked a few error messages (#649) * Made a number of tweaks to the MANIFEST and tweaked a few error messages * Added Docs back to manifest * Added mock to tests_require --- MANIFEST.in | 6 +++--- arrow/arrow.py | 11 ++++++++--- requirements.txt | 6 +++--- setup.py | 4 ++-- tests/arrow_tests.py | 7 ++++++- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 720c0aac0..3eafcd8f7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include LICENSE HISTORY.md README.rst -recursive-include tests * -recursive-include docs * +include LICENSE CHANGELOG.md README.rst +recursive-include tests *.py +recursive-include docs *.py *.rst *.bat Makefile diff --git a/arrow/arrow.py b/arrow/arrow.py index fa84b22a9..5ff96bddd 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -615,7 +615,7 @@ def shift(self, **kwargs): """ Returns a new :class:`Arrow ` object with attributes updated according to inputs. - Use pluralized property names to shift their current value relatively: + Use pluralized property names to relatively shift their current value: >>> import arrow >>> arw = arrow.utcnow() @@ -642,13 +642,18 @@ def shift(self, **kwargs): """ relative_kwargs = {} + additional_attrs = ["weeks", "quarters", "weekday"] for key, value in kwargs.items(): - if key in self._ATTRS_PLURAL or key in ["weeks", "quarters", "weekday"]: + if key in self._ATTRS_PLURAL or key in additional_attrs: relative_kwargs[key] = value else: - raise AttributeError() + raise AttributeError( + "Invalid shift time frame. Please select one of the following: {}.".format( + ", ".join(self._ATTRS_PLURAL + additional_attrs) + ) + ) # core datetime does not support quarters, translate to months. relative_kwargs.setdefault("months", 0) diff --git a/requirements.txt b/requirements.txt index e3b1a1e9f..55b22311f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -backports.functools_lru_cache==1.5.0 +backports.functools_lru_cache==1.5.0; python_version == "2.7" chai==1.1.2 mock==3.0.* nose==1.3.7 @@ -7,5 +7,5 @@ pre-commit==1.18.* python-dateutil==2.8.* pytz==2019.* simplejson==3.16.* -sphinx==1.8.*; python_version == '2.7' -sphinx==2.2.*; python_version >= '3.5' +sphinx==1.8.*; python_version == "2.7" +sphinx==2.2.*; python_version >= "3.5" diff --git a/setup.py b/setup.py index dec463a4e..8778bebf1 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ description="Better dates & times for Python", long_description=readme, long_description_content_type="text/x-rst", - url="https://arrow.readthedocs.io/en/latest/", + url="https://arrow.readthedocs.io", author="Chris Smith", author_email="crsmithdev@gmail.com", license="Apache 2.0", @@ -28,7 +28,7 @@ "backports.functools_lru_cache>=1.2.1;python_version=='2.7'", ], test_suite="tests", - tests_require=["chai"], + tests_require=["chai", "mock"], classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 5e1baf4fc..ef6928765 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -564,8 +564,13 @@ def test_replace_other_kwargs(self): class ArrowShiftTests(Chai): def test_not_attr(self): + now = arrow.Arrow.utcnow() + + with self.assertRaises(AttributeError): + now.shift(abc=1) + with self.assertRaises(AttributeError): - arrow.Arrow.utcnow().shift(abc=1) + now.shift(week=1) def test_shift(self): From f02629468724561f0ae2af28684cc25644674664 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 27 Aug 2019 19:50:54 +0300 Subject: [PATCH 275/649] Increment version to 0.14.6 and add to CHANGELOG (#648) --- CHANGELOG.md | 9 +++++++++ arrow/_version.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edab726f5..473f540e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.14.6 + +- [NEW] Added support for `week` granularity in `Arrow.humanize()`. For example, `arrow.utcnow().shift(weeks=-1).humanize(granularity="week")` outputs "a week ago". This change introduced two new untranslated words, `week` and `weeks`, to all locale dictionaries, so locale contributions are welcome! +- [NEW] Fully translated the Brazilian Portugese locale. +- [CHANGE] Updated the Macedonian locale to inherit from a Slavic base. +- [FIX] Fixed a bug that caused `arrow.get()` to ignore tzinfo arguments of type string (e.g. `arrow.get(tzinfo="Europe/Paris")`). +- [FIX] Fixed a bug that occurred when `arrow.Arrow()` was instantiated with a `pytz` tzinfo object. +- [FIX] Fixed a bug that caused Arrow to fail when passed a sub-second token, that when rounded, had a value greater than 999999 (e.g. `arrow.get("2015-01-12T01:13:15.9999995")`). Arrow should now accurately propagate the rounding for large sub-second tokens. + ## 0.14.5 - [NEW] Added Afrikaans locale. diff --git a/arrow/_version.py b/arrow/_version.py index 141826d55..f2c260cc1 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.14.5" +__version__ = "0.14.6" From b37a0a1a5691f3db9faca1eb0d71f165c566b2ca Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Wed, 4 Sep 2019 15:07:49 -0400 Subject: [PATCH 276/649] Tweaked when ArrowParseWarning is called and incremented version to 0.14.7 --- CHANGELOG.md | 4 ++++ arrow/_version.py | 2 +- arrow/factory.py | 3 --- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 473f540e1..ef72eb7e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.14.7 + +- [CHANGE] `ArrowParseWarning` will no longer be printed on every call to `arrow.get()` with a datetime string. The purpose of the warning was to start a conversation about the upcoming 0.15.0 changes and we appreciate all the feedback that the community has given us! + ## 0.14.6 - [NEW] Added support for `week` granularity in `Arrow.humanize()`. For example, `arrow.utcnow().shift(weeks=-1).humanize(granularity="week")` outputs "a week ago". This change introduced two new untranslated words, `week` and `weeks`, to all locale dictionaries, so locale contributions are welcome! diff --git a/arrow/_version.py b/arrow/_version.py index f2c260cc1..e1130451f 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.14.6" +__version__ = "0.14.7" diff --git a/arrow/factory.py b/arrow/factory.py index aed4815c1..d1c233c06 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -33,9 +33,6 @@ class ArrowParseWarning(DeprecationWarning): """ -warnings.simplefilter("always", ArrowParseWarning) - - class ArrowFactory(object): """ A factory for generating :class:`Arrow ` objects. From e6bafb30bc562affa24715197e4c762e10e06e31 Mon Sep 17 00:00:00 2001 From: Yoan Tournade Date: Thu, 5 Sep 2019 16:38:19 +0200 Subject: [PATCH 277/649] Add French locale for week granularity (#652) --- arrow/locales.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index ae4bff71f..3f59e34ca 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -445,6 +445,8 @@ class FrenchLocale(Locale): "hours": "{0} heures", "day": "un jour", "days": "{0} jours", + "week": "une semaine", + "weeks": "{0} semaines", "month": "un mois", "months": "{0} mois", "year": "un an", From c35f839db2d349397e35a03f96a05fc8988c5e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Thu, 5 Sep 2019 17:36:09 +0200 Subject: [PATCH 278/649] Add Swiss German translations for week(s) --- arrow/locales.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 3f59e34ca..42c101225 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -3610,6 +3610,8 @@ class SwissLocale(Locale): "hours": "{0} Stunden", "day": "einem Tag", "days": "{0} Tagen", + "week": "einer Woche", + "weeks": "{0} Wochen", "month": "einem Monat", "months": "{0} Monaten", "year": "einem Jahr", From 877debfc965f1258ad3da51823cf5b77a20bbcae Mon Sep 17 00:00:00 2001 From: rdtq Date: Fri, 6 Sep 2019 19:49:15 +0300 Subject: [PATCH 279/649] Update russian translations for week(s) (#654) Update russian translations for week(s) --- arrow/locales.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 42c101225..8f34dbe9d 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1191,6 +1191,8 @@ class RussianLocale(SlavicBaseLocale): "hours": ["{0} час", "{0} часа", "{0} часов"], "day": "день", "days": ["{0} день", "{0} дня", "{0} дней"], + "week": "неделю", + "weeks": ["{0} неделю", "{0} недели", "{0} недель"], "month": "месяц", "months": ["{0} месяц", "{0} месяца", "{0} месяцев"], "year": "год", From 66e97fea193b6e3d53bceafcd5d4b237c54aea59 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 8 Sep 2019 11:15:41 -0400 Subject: [PATCH 280/649] Changed checking of basic time format --- arrow/parser.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 3ce85f225..952131fd1 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -151,10 +151,8 @@ def parse_iso(self, datetime_string): date_string, time_string = datetime_string.split("T", 1) time_parts = re.split(r"[\+\-Z]", time_string, 1, re.IGNORECASE) - # TODO: should we prevent mixing basic and extended formats? would need to ensure that dates, times, and timezones are in same format - time_colon_count = time_parts[0].count(":") - is_basic_time_format = time_colon_count == 0 + is_basic_time_format = ":" not in time_parts[0] tz_format = "Z" # use 'ZZ' token instead since tz offset is present in non-basic format From 9aa2cb5138b150a60410a9505284517c4f4ef8d7 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 8 Sep 2019 11:47:50 -0400 Subject: [PATCH 281/649] Upgraded pre-commit packages --- .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 22aa58715..f8f133f43 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v1.22.1 + rev: v1.23.0 hooks: - id: pyupgrade - repo: https://github.com/pre-commit/pygrep-hooks @@ -33,7 +33,7 @@ repos: - id: python-no-eval - id: python-check-blanket-noqa - id: rst-backticks - - repo: https://github.com/python/black + - repo: https://github.com/psf/black rev: 19.3b0 hooks: - id: black From ca1bb57cb41a3b7b1a314e6c69f091d5fdac6d84 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 8 Sep 2019 15:17:33 -0400 Subject: [PATCH 282/649] Added tests for leading and trailing whitespace --- arrow/parser.py | 3 +-- tests/parser_tests.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 8e3f6563f..716bbe968 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -125,9 +125,8 @@ def parse_iso(self, datetime_string): has_time = has_space_divider or has_t_divider has_tz = False - # TODO: add tests for all the new formats, especially basic format - # NOTE: YYYYMM is omitted to avoid confusion with YYMMDD (no longer part of ISO 8601, but is still often used) # date formats (ISO-8601 and others) to test against + # NOTE: YYYYMM is omitted to avoid confusion with YYMMDD (no longer part of ISO 8601, but is still often used) formats = [ "YYYY-MM-DD", "YYYY-M-DD", diff --git a/tests/parser_tests.py b/tests/parser_tests.py index e020df7eb..d13433033 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -1002,6 +1002,42 @@ def test_isoformat(self): self.assertEqual(self.parser.parse_iso(dt.isoformat()), dt) + def test_parse_iso_with_leading_and_trailing_whitespace(self): + datetime_string = " 2016-11-15T06:37:19.123456" + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + + datetime_string = " 2016-11-15T06:37:19.123456 " + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + + datetime_string = "2016-11-15T06:37:19.123456 " + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + + datetime_string = "2016-11-15T 06:37:19.123456" + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + + # leading whitespace + datetime_string = " 2016-11-15 06:37:19.123456" + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + + # trailing whitespace + datetime_string = "2016-11-15 06:37:19.123456 " + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + + datetime_string = " 2016-11-15 06:37:19.123456 " + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + + # two dividing spaces + datetime_string = "2016-11-15 06:37:19.123456" + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + def test_parse_iso_with_extra_words_at_start_and_end_invalid(self): test_inputs = [ "blah2016", From 3160c4c20c08ef5c0b59a4297edb9e32d32c2d60 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 8 Sep 2019 15:48:43 -0400 Subject: [PATCH 283/649] Added test for comma subsecond separator and moved setting of tz_format down in file --- arrow/parser.py | 14 +++++++------- tests/parser_tests.py | 6 ++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 716bbe968..f1469a00b 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -155,13 +155,6 @@ def parse_iso(self, datetime_string): time_parts = re.split(r"[\+\-Z]", time_string, 1, re.IGNORECASE) - is_basic_time_format = ":" not in time_parts[0] - tz_format = "Z" - - # use 'ZZ' token instead since tz offset is present in non-basic format - if len(time_parts) == 2 and ":" in time_parts[1]: - tz_format = "ZZ" - time_components = self._TIME_RE.match(time_parts[0]) if time_components is None: @@ -178,6 +171,13 @@ def parse_iso(self, datetime_string): has_seconds = seconds is not None has_subseconds = subseconds is not None + is_basic_time_format = ":" not in time_parts[0] + tz_format = "Z" + + # use 'ZZ' token instead since tz offset is present in non-basic format + if has_tz and ":" in time_parts[1]: + tz_format = "ZZ" + time_sep = "" if is_basic_time_format else ":" if has_subseconds: diff --git a/tests/parser_tests.py b/tests/parser_tests.py index d13433033..36bafcc9d 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -976,6 +976,12 @@ def test_parse_subsecond(self): self.parser.parse_iso("2013-01-01 12:30:45.987654"), self.expected ) + # use comma as subsecond separator + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) + self.assertEqual( + self.parser.parse_iso("2013-01-01 12:30:45,987654"), self.expected + ) + def test_gnu_date(self): """ regression tests for parsing output from GNU date(1) From ee2b3b3243a6c4b90a4e0b6155acbbfc97bf3270 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Sun, 8 Sep 2019 21:03:46 +0100 Subject: [PATCH 284/649] Update CHANGELOG for 0.15.0 and bump version --- CHANGELOG.md | 48 +++++++++++++++++++++++++++++++++++++++++++++++ arrow/_version.py | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef72eb7e4..5f5de71da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## 0.15.0 +- [NEW] Added support for DDD and DDDD ordinal date tokens. The following functionality is now possible: `arrow.get("1998-045")`, `arrow.get("1998-45", "YYYY-DDD")`, `arrow.get("1998-045", "YYYY-DDDD")`. +- [NEW] ISO-8601 basic format is now supported (e.g. `YYYYMMDDTHHmmssZ`). +- [NEW] Added week granularity translations for French, Russian and Swiss German locales. +- [CHANGE] Timestamps of type `str` are no longer supported **without a format string** in the arrow.get() method. This change was made to support the ISO 8601 basic format and to address bugs such as [#447](https://github.com/crsmithdev/arrow/issues/447). + +```python + +# will NOT work in v0.15.0 + +arrow.get("1565358758") + +arrow.get("1565358758.123413") + +# will work in v0.15.0 + +arrow.get("1565358758", "X") + +arrow.get("1565358758.123413", "X") + +arrow.get(1565358758) + +arrow.get(1565358758.123413) + +``` + +- [CHANGE] When a meridian token (a|A) is passed and no meridians are available for the specified locale (e.g. unsupported or untranslated) a `ParserError` is raised. +- [CHANGE] The timestamp token (`X`) will now match float timestamps of type `str`: `arrow.get(“1565358758.123415”, “X”). +- [CHANGE] Strings with leading or trailing whitespace will no longer be parsed without a format string, please see [the docs](https://arrow.readthedocs.io/en/latest/#regular-expressions) for more info. +- [FIX] The timestamp token (`X`) will now only match on strings that strictly contain integers and floats, preventing incorrect matches. +- [FIX] Most instances of `arrow.get()` returning an incorrect arrow object from a partial parsing match have been eliminated, see examples below. + +```python + +arrow.get(“garbage2017everywhere”) + + + +``` + +```python + +arrow.get(‘Jun-2019’, [‘MMM-YY’, ‘MMM-YYYY’]) + + + +``` + ## 0.14.7 - [CHANGE] `ArrowParseWarning` will no longer be printed on every call to `arrow.get()` with a datetime string. The purpose of the warning was to start a conversation about the upcoming 0.15.0 changes and we appreciate all the feedback that the community has given us! diff --git a/arrow/_version.py b/arrow/_version.py index e1130451f..9da2f8fcc 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.14.7" +__version__ = "0.15.0" From 88ca1f718e2f676fa3e54950af2a57675354ad3c Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Sun, 8 Sep 2019 21:17:42 +0100 Subject: [PATCH 285/649] Minor tweaks to changelog --- CHANGELOG.md | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f5de71da..156a14cea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,46 +7,22 @@ - [CHANGE] Timestamps of type `str` are no longer supported **without a format string** in the arrow.get() method. This change was made to support the ISO 8601 basic format and to address bugs such as [#447](https://github.com/crsmithdev/arrow/issues/447). ```python - # will NOT work in v0.15.0 - arrow.get("1565358758") - arrow.get("1565358758.123413") # will work in v0.15.0 - arrow.get("1565358758", "X") - arrow.get("1565358758.123413", "X") - arrow.get(1565358758) - arrow.get(1565358758.123413) - ``` - [CHANGE] When a meridian token (a|A) is passed and no meridians are available for the specified locale (e.g. unsupported or untranslated) a `ParserError` is raised. -- [CHANGE] The timestamp token (`X`) will now match float timestamps of type `str`: `arrow.get(“1565358758.123415”, “X”). +- [CHANGE] The timestamp token (`X`) will now match float timestamps of type `str`: `arrow.get(“1565358758.123415”, “X”)`. - [CHANGE] Strings with leading or trailing whitespace will no longer be parsed without a format string, please see [the docs](https://arrow.readthedocs.io/en/latest/#regular-expressions) for more info. -- [FIX] The timestamp token (`X`) will now only match on strings that strictly contain integers and floats, preventing incorrect matches. -- [FIX] Most instances of `arrow.get()` returning an incorrect arrow object from a partial parsing match have been eliminated, see examples below. - -```python - -arrow.get(“garbage2017everywhere”) - - - -``` - -```python - -arrow.get(‘Jun-2019’, [‘MMM-YY’, ‘MMM-YYYY’]) - - - -``` +- [FIX] The timestamp token (`X`) will now only match on strings that **strictly contain integers and floats**, preventing incorrect matches. +- [FIX] Most instances of `arrow.get()` returning an incorrect arrow object from a partial parsing match have been eliminated, the following issue have been addressed: https://github.com/crsmithdev/arrow/issues/91, https://github.com/crsmithdev/arrow/issues/196, https://github.com/crsmithdev/arrow/issues/396, https://github.com/crsmithdev/arrow/issues/434, https://github.com/crsmithdev/arrow/issues/447, https://github.com/crsmithdev/arrow/issues/456, https://github.com/crsmithdev/arrow/issues/519, https://github.com/crsmithdev/arrow/issues/538, https://github.com/crsmithdev/arrow/issues/560. ## 0.14.7 From 848452d2591941e0ad48cef7b64e233dcd72578b Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Sun, 8 Sep 2019 21:24:45 +0100 Subject: [PATCH 286/649] More minor tweaks --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 156a14cea..ca4c7056f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 0.15.0 - [NEW] Added support for DDD and DDDD ordinal date tokens. The following functionality is now possible: `arrow.get("1998-045")`, `arrow.get("1998-45", "YYYY-DDD")`, `arrow.get("1998-045", "YYYY-DDDD")`. -- [NEW] ISO-8601 basic format is now supported (e.g. `YYYYMMDDTHHmmssZ`). +- [NEW] ISO 8601 basic format is now supported (e.g. `YYYYMMDDTHHmmssZ`). - [NEW] Added week granularity translations for French, Russian and Swiss German locales. - [CHANGE] Timestamps of type `str` are no longer supported **without a format string** in the arrow.get() method. This change was made to support the ISO 8601 basic format and to address bugs such as [#447](https://github.com/crsmithdev/arrow/issues/447). @@ -22,7 +22,7 @@ arrow.get(1565358758.123413) - [CHANGE] The timestamp token (`X`) will now match float timestamps of type `str`: `arrow.get(“1565358758.123415”, “X”)`. - [CHANGE] Strings with leading or trailing whitespace will no longer be parsed without a format string, please see [the docs](https://arrow.readthedocs.io/en/latest/#regular-expressions) for more info. - [FIX] The timestamp token (`X`) will now only match on strings that **strictly contain integers and floats**, preventing incorrect matches. -- [FIX] Most instances of `arrow.get()` returning an incorrect arrow object from a partial parsing match have been eliminated, the following issue have been addressed: https://github.com/crsmithdev/arrow/issues/91, https://github.com/crsmithdev/arrow/issues/196, https://github.com/crsmithdev/arrow/issues/396, https://github.com/crsmithdev/arrow/issues/434, https://github.com/crsmithdev/arrow/issues/447, https://github.com/crsmithdev/arrow/issues/456, https://github.com/crsmithdev/arrow/issues/519, https://github.com/crsmithdev/arrow/issues/538, https://github.com/crsmithdev/arrow/issues/560. +- [FIX] Most instances of `arrow.get()` returning an incorrect arrow object from a partial parsing match have been eliminated, the following issue have been addressed: [#91](https://github.com/crsmithdev/arrow/issues/91), [#196](https://github.com/crsmithdev/arrow/issues/196), [#396](https://github.com/crsmithdev/arrow/issues/396), [#434](https://github.com/crsmithdev/arrow/issues/434), [#447](https://github.com/crsmithdev/arrow/issues/447), [#456](https://github.com/crsmithdev/arrow/issues/456), [#519](https://github.com/crsmithdev/arrow/issues/519), [#538](https://github.com/crsmithdev/arrow/issues/538), [#560](https://github.com/crsmithdev/arrow/issues/560). ## 0.14.7 From a83cef8aeb86b45d6c9dd2a1c88921bc1139c242 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Sun, 8 Sep 2019 21:36:22 +0100 Subject: [PATCH 287/649] Make Jad happy --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca4c7056f..5459a17cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,9 @@ ## 0.15.0 - [NEW] Added support for DDD and DDDD ordinal date tokens. The following functionality is now possible: `arrow.get("1998-045")`, `arrow.get("1998-45", "YYYY-DDD")`, `arrow.get("1998-045", "YYYY-DDDD")`. -- [NEW] ISO 8601 basic format is now supported (e.g. `YYYYMMDDTHHmmssZ`). -- [NEW] Added week granularity translations for French, Russian and Swiss German locales. -- [CHANGE] Timestamps of type `str` are no longer supported **without a format string** in the arrow.get() method. This change was made to support the ISO 8601 basic format and to address bugs such as [#447](https://github.com/crsmithdev/arrow/issues/447). +- [NEW] ISO 8601 basic format for dates and times is now supported (e.g. `YYYYMMDDTHHmmssZ`). +- [NEW] Added `humanize` week granularity translations for French, Russian and Swiss German locales. +- [CHANGE] Timestamps of type `str` are no longer supported **without a format string** in the `arrow.get()` method. This change was made to support the ISO 8601 basic format and to address bugs such as [#447](https://github.com/crsmithdev/arrow/issues/447). ```python # will NOT work in v0.15.0 @@ -20,9 +20,9 @@ arrow.get(1565358758.123413) - [CHANGE] When a meridian token (a|A) is passed and no meridians are available for the specified locale (e.g. unsupported or untranslated) a `ParserError` is raised. - [CHANGE] The timestamp token (`X`) will now match float timestamps of type `str`: `arrow.get(“1565358758.123415”, “X”)`. -- [CHANGE] Strings with leading or trailing whitespace will no longer be parsed without a format string, please see [the docs](https://arrow.readthedocs.io/en/latest/#regular-expressions) for more info. +- [CHANGE] Strings with leading and/or trailing whitespace will no longer be parsed without a format string. Please see [the docs](https://arrow.readthedocs.io/en/latest/#regular-expressions) for ways to handle this. - [FIX] The timestamp token (`X`) will now only match on strings that **strictly contain integers and floats**, preventing incorrect matches. -- [FIX] Most instances of `arrow.get()` returning an incorrect arrow object from a partial parsing match have been eliminated, the following issue have been addressed: [#91](https://github.com/crsmithdev/arrow/issues/91), [#196](https://github.com/crsmithdev/arrow/issues/196), [#396](https://github.com/crsmithdev/arrow/issues/396), [#434](https://github.com/crsmithdev/arrow/issues/434), [#447](https://github.com/crsmithdev/arrow/issues/447), [#456](https://github.com/crsmithdev/arrow/issues/456), [#519](https://github.com/crsmithdev/arrow/issues/519), [#538](https://github.com/crsmithdev/arrow/issues/538), [#560](https://github.com/crsmithdev/arrow/issues/560). +- [FIX] Most instances of `arrow.get()` returning an incorrect `Arrow` object from a partial parsing match have been eliminated. The following issue have been addressed: [#91](https://github.com/crsmithdev/arrow/issues/91), [#196](https://github.com/crsmithdev/arrow/issues/196), [#396](https://github.com/crsmithdev/arrow/issues/396), [#434](https://github.com/crsmithdev/arrow/issues/434), [#447](https://github.com/crsmithdev/arrow/issues/447), [#456](https://github.com/crsmithdev/arrow/issues/456), [#519](https://github.com/crsmithdev/arrow/issues/519), [#538](https://github.com/crsmithdev/arrow/issues/538), [#560](https://github.com/crsmithdev/arrow/issues/560). ## 0.14.7 From 9b0521697b3d0cc4ef88eab43828109347aa3aca Mon Sep 17 00:00:00 2001 From: Ryuuji Yoshimoto Date: Tue, 10 Sep 2019 19:00:03 +0900 Subject: [PATCH 288/649] Add Japanese translations for week(s) (#659) --- arrow/locales.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 8f34dbe9d..203411c27 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -588,6 +588,8 @@ class JapaneseLocale(Locale): "hours": "{0}時間", "day": "1日", "days": "{0}日", + "week": "1週間", + "weeks": "{0}週間", "month": "1ヶ月", "months": "{0}ヶ月", "year": "1年", From d856efa1dd81f45b1d2400e4304c35d9ec68b76f Mon Sep 17 00:00:00 2001 From: Joseph Kahn Date: Tue, 10 Sep 2019 12:20:34 -0400 Subject: [PATCH 289/649] Update parser.py --- arrow/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/parser.py b/arrow/parser.py index f1469a00b..2720d3c18 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -45,7 +45,7 @@ class DateTimeParser(object): _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will # break cases like "15 Jul 2000" and a format list (see issue #447) - _TIMESTAMP_RE = re.compile(r"^\d+\.?\d+$") + _TIMESTAMP_RE = re.compile(r"^-?\d+\.?\d+$") _TIME_RE = re.compile(r"^(\d{2})(?:\:?(\d{2}))?(?:\:?(\d{2}))?(?:([\.\,])(\d+))?$") _BASE_INPUT_RE_MAP = { From 45d12d46dc9a18e9868b5fe5b509d164db430c8c Mon Sep 17 00:00:00 2001 From: Joseph Kahn Date: Tue, 10 Sep 2019 12:23:50 -0400 Subject: [PATCH 290/649] Update parser_tests.py --- tests/parser_tests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 36bafcc9d..cbaeb27a7 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -224,6 +224,12 @@ def test_parse_timestamp(self): self.parser.parse("{:f}123456".format(float_timestamp), "X"), self.expected ) + negative_timestamp = -1565358758 + self.expected = datetime.fromtimestamp(negative_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:d}".format(negative_timestamp), "X"), self.expected + ) + # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will # break cases like "15 Jul 2000" and a format list (see issue #447) with self.assertRaises(ParserError): From 1974316d8930c01abf57aee68a294bd0fd37b0b6 Mon Sep 17 00:00:00 2001 From: Joseph Kahn Date: Tue, 10 Sep 2019 12:26:51 -0400 Subject: [PATCH 291/649] linting --- tests/parser_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index cbaeb27a7..0fe754331 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -228,7 +228,7 @@ def test_parse_timestamp(self): self.expected = datetime.fromtimestamp(negative_timestamp, tz=tz_utc) self.assertEqual( self.parser.parse("{:d}".format(negative_timestamp), "X"), self.expected - ) + ) # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will # break cases like "15 Jul 2000" and a format list (see issue #447) From dafdb69f1dc0c6c6b1fb8bf2aa07383fbb2bed78 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 10 Sep 2019 13:39:21 -0400 Subject: [PATCH 292/649] Fix bug with tzinfo.zone --- arrow/arrow.py | 1 + requirements.txt | 1 + setup.cfg | 2 +- tests/factory_tests.py | 9 +++++++++ tests/parser_tests.py | 14 +++++++++++--- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 9ed179c15..1b05b42bb 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -66,6 +66,7 @@ def __init__( elif ( isinstance(tzinfo, dt_tzinfo) and hasattr(tzinfo, "localize") + and hasattr(tzinfo, "zone") and tzinfo.zone ): tzinfo = parser.TzinfoParser.parse(tzinfo.zone) diff --git a/requirements.txt b/requirements.txt index 55b22311f..780f15d9b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ backports.functools_lru_cache==1.5.0; python_version == "2.7" chai==1.1.2 +dateparser==0.7.* mock==3.0.* nose==1.3.7 nose-cov==1.6 diff --git a/setup.cfg b/setup.cfg index d93e3fc9b..08cae3ddf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ ignore = E203,E501,W503 line_length = 88 multi_line_output = 3 include_trailing_comma = true -known_third_party = chai,dateutil,mock,pytz,setuptools,simplejson +known_third_party = chai,dateparser,dateutil,mock,pytz,setuptools,simplejson [bdist_wheel] universal = 1 diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 0e89ffb01..3c9c26590 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -2,6 +2,7 @@ import time from datetime import date, datetime +import dateparser from chai import Chai from dateutil import tz @@ -102,6 +103,14 @@ def test_one_arg_tzinfo(self): assertDtEqual(self.factory.get(tz.gettz("US/Pacific")), self.expected) + # regression test for issue #658 + def test_one_arg_dateparser_datetime(self): + expected = datetime(1990, 1, 1).replace(tzinfo=tz.tzutc()) + # dateparser outputs: datetime.datetime(1990, 1, 1, 0, 0, tzinfo=) + parsed_date = dateparser.parse("1990-01-01T00:00:00+00:00") + arrow_obj = self.factory.get(parsed_date)._datetime.replace(tzinfo=tz.tzutc()) + self.assertEqual(arrow_obj, expected) + def test_kwarg_tzinfo(self): self.expected = ( diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 0fe754331..85d8ad07d 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -224,10 +224,18 @@ def test_parse_timestamp(self): self.parser.parse("{:f}123456".format(float_timestamp), "X"), self.expected ) - negative_timestamp = -1565358758 - self.expected = datetime.fromtimestamp(negative_timestamp, tz=tz_utc) + # regression test for issue #662 + negative_int_timestamp = -int_timestamp + self.expected = datetime.fromtimestamp(negative_int_timestamp, tz=tz_utc) self.assertEqual( - self.parser.parse("{:d}".format(negative_timestamp), "X"), self.expected + self.parser.parse("{:d}".format(negative_int_timestamp), "X"), self.expected + ) + + negative_float_timestamp = -float_timestamp + self.expected = datetime.fromtimestamp(negative_float_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:f}".format(negative_float_timestamp), "X"), + self.expected, ) # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will From f49cd84a73f048cdfaad40f861273ec2d2bd443f Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 10 Sep 2019 13:42:31 -0400 Subject: [PATCH 293/649] Renamed variable --- tests/factory_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 3c9c26590..bccc16376 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -108,8 +108,8 @@ def test_one_arg_dateparser_datetime(self): expected = datetime(1990, 1, 1).replace(tzinfo=tz.tzutc()) # dateparser outputs: datetime.datetime(1990, 1, 1, 0, 0, tzinfo=) parsed_date = dateparser.parse("1990-01-01T00:00:00+00:00") - arrow_obj = self.factory.get(parsed_date)._datetime.replace(tzinfo=tz.tzutc()) - self.assertEqual(arrow_obj, expected) + dt_output = self.factory.get(parsed_date)._datetime.replace(tzinfo=tz.tzutc()) + self.assertEqual(dt_output, expected) def test_kwarg_tzinfo(self): From 15fae21f01ae514e2a36d3b6107b61a851588175 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 10 Sep 2019 13:47:12 -0400 Subject: [PATCH 294/649] Prep 0.15.1 release --- CHANGELOG.md | 5 +++++ arrow/_version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5459a17cd..16073aa7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.15.1 + +- [FIX] Fixed a bug that caused Arrow to fail when passed a negative timestamp string. +- [FIX] Fixed a bug that caused Arrow to fail when passed a datetime object with `tzinfo` of type `StaticTzInfo`. + ## 0.15.0 - [NEW] Added support for DDD and DDDD ordinal date tokens. The following functionality is now possible: `arrow.get("1998-045")`, `arrow.get("1998-45", "YYYY-DDD")`, `arrow.get("1998-045", "YYYY-DDDD")`. - [NEW] ISO 8601 basic format for dates and times is now supported (e.g. `YYYYMMDDTHHmmssZ`). diff --git a/arrow/_version.py b/arrow/_version.py index 9da2f8fcc..903e77ce1 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.15.0" +__version__ = "0.15.1" From 8f073f20bc888fabbd82ed174b7b6b8d17d07bd3 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Tue, 10 Sep 2019 21:20:19 +0100 Subject: [PATCH 295/649] Add changelog to docs --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 78fcea57c..457e59f2a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ Arrow: Better dates & times for Python ====================================== -Release v\ |release|. (`Installation`_) +Release v\ |release|. (`Installation`_) (`Changelog `_) .. include:: ../README.rst :start-after: start-inclusion-marker-do-not-remove From b43d7d46096a04a426223c93bc0bf9ee9ef9c40f Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Wed, 11 Sep 2019 04:02:26 -0600 Subject: [PATCH 296/649] Add Travis config for Windows (#669) * Attempt Travis config for Windows * Tweak config * Test config * Ignore negative tests on Windows --- .travis.yml | 9 +++++++++ CHANGELOG.md | 1 + tests/parser_tests.py | 29 +++++++++++++++++------------ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7087e8f3e..ba993ab8b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,15 @@ matrix: - name: "Python 3.7" python: "3.7" env: TOXENV=py37 + - name: "Python 3.7 on Windows" + os: windows + language: shell # 'language: python' is an error on Travis CI Windows + before_install: + - choco install python --version 3.7.4 + - python -m pip install --upgrade pip + env: + - PATH=/c/Python37:/c/Python37/Scripts:$PATH + - TOXENV=py37 - name: "Python 3.8" python: "3.8-dev" env: TOXENV=py38 diff --git a/CHANGELOG.md b/CHANGELOG.md index 16073aa7a..f65944a2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.15.1 +- [NEW] Added `humanize` week granularity translations for Japanese. - [FIX] Fixed a bug that caused Arrow to fail when passed a negative timestamp string. - [FIX] Fixed a bug that caused Arrow to fail when passed a datetime object with `tzinfo` of type `StaticTzInfo`. diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 85d8ad07d..48cff7dc8 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import calendar +import os import time from datetime import datetime @@ -224,19 +225,23 @@ def test_parse_timestamp(self): self.parser.parse("{:f}123456".format(float_timestamp), "X"), self.expected ) - # regression test for issue #662 - negative_int_timestamp = -int_timestamp - self.expected = datetime.fromtimestamp(negative_int_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:d}".format(negative_int_timestamp), "X"), self.expected - ) + # NOTE: negative timestamps cannot be handled by datetime on Window + # Must use timedelta to handle them. ref: https://stackoverflow.com/questions/36179914 + if os.name != "nt": + # regression test for issue #662 + negative_int_timestamp = -int_timestamp + self.expected = datetime.fromtimestamp(negative_int_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:d}".format(negative_int_timestamp), "X"), + self.expected, + ) - negative_float_timestamp = -float_timestamp - self.expected = datetime.fromtimestamp(negative_float_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:f}".format(negative_float_timestamp), "X"), - self.expected, - ) + negative_float_timestamp = -float_timestamp + self.expected = datetime.fromtimestamp(negative_float_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:f}".format(negative_float_timestamp), "X"), + self.expected, + ) # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will # break cases like "15 Jul 2000" and a format list (see issue #447) From cd11a149462dea9826633c2ab860d848b996a82d Mon Sep 17 00:00:00 2001 From: Pedrat Date: Thu, 12 Sep 2019 15:48:36 +0100 Subject: [PATCH 297/649] Adds week/weeks to Portuguese and BrazilianPortuguese locales --- arrow/locales.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 203411c27..930b6b5b6 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1784,6 +1784,8 @@ class PortugueseLocale(Locale): "hours": "{0} horas", "day": "um dia", "days": "{0} dias", + "week": "uma semana", + "weeks": "{0} semanas", "month": "um mês", "months": "{0} meses", "year": "um ano", @@ -1850,6 +1852,8 @@ class BrazilianPortugueseLocale(PortugueseLocale): "hours": "{0} horas", "day": "um dia", "days": "{0} dias", + "week": "uma semana", + "weeks": "{0} semanas", "month": "um mês", "months": "{0} meses", "year": "um ano", From c3364d8ec7e6be0b8514cf5a1b7635904c4f208a Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Thu, 12 Sep 2019 17:03:47 +0100 Subject: [PATCH 298/649] Date versions in changelog --- CHANGELOG.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16073aa7a..71dbb3a25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ # Changelog -## 0.15.1 +## 0.15.1 (2019-09-10) - [FIX] Fixed a bug that caused Arrow to fail when passed a negative timestamp string. - [FIX] Fixed a bug that caused Arrow to fail when passed a datetime object with `tzinfo` of type `StaticTzInfo`. -## 0.15.0 +## 0.15.0 (2019-09-08) - [NEW] Added support for DDD and DDDD ordinal date tokens. The following functionality is now possible: `arrow.get("1998-045")`, `arrow.get("1998-45", "YYYY-DDD")`, `arrow.get("1998-045", "YYYY-DDDD")`. - [NEW] ISO 8601 basic format for dates and times is now supported (e.g. `YYYYMMDDTHHmmssZ`). - [NEW] Added `humanize` week granularity translations for French, Russian and Swiss German locales. @@ -29,11 +29,11 @@ arrow.get(1565358758.123413) - [FIX] The timestamp token (`X`) will now only match on strings that **strictly contain integers and floats**, preventing incorrect matches. - [FIX] Most instances of `arrow.get()` returning an incorrect `Arrow` object from a partial parsing match have been eliminated. The following issue have been addressed: [#91](https://github.com/crsmithdev/arrow/issues/91), [#196](https://github.com/crsmithdev/arrow/issues/196), [#396](https://github.com/crsmithdev/arrow/issues/396), [#434](https://github.com/crsmithdev/arrow/issues/434), [#447](https://github.com/crsmithdev/arrow/issues/447), [#456](https://github.com/crsmithdev/arrow/issues/456), [#519](https://github.com/crsmithdev/arrow/issues/519), [#538](https://github.com/crsmithdev/arrow/issues/538), [#560](https://github.com/crsmithdev/arrow/issues/560). -## 0.14.7 +## 0.14.7 (2019-09-04) - [CHANGE] `ArrowParseWarning` will no longer be printed on every call to `arrow.get()` with a datetime string. The purpose of the warning was to start a conversation about the upcoming 0.15.0 changes and we appreciate all the feedback that the community has given us! -## 0.14.6 +## 0.14.6 (2019-08-28) - [NEW] Added support for `week` granularity in `Arrow.humanize()`. For example, `arrow.utcnow().shift(weeks=-1).humanize(granularity="week")` outputs "a week ago". This change introduced two new untranslated words, `week` and `weeks`, to all locale dictionaries, so locale contributions are welcome! - [NEW] Fully translated the Brazilian Portugese locale. @@ -42,18 +42,18 @@ arrow.get(1565358758.123413) - [FIX] Fixed a bug that occurred when `arrow.Arrow()` was instantiated with a `pytz` tzinfo object. - [FIX] Fixed a bug that caused Arrow to fail when passed a sub-second token, that when rounded, had a value greater than 999999 (e.g. `arrow.get("2015-01-12T01:13:15.9999995")`). Arrow should now accurately propagate the rounding for large sub-second tokens. -## 0.14.5 +## 0.14.5 (2019-08-09) - [NEW] Added Afrikaans locale. - [CHANGE] Removed deprecated `replace` shift functionality. Users looking to pass plural properties to the `replace` function to shift values should use `shift` instead. - [FIX] Fixed bug that occurred when `factory.get()` was passed a locale kwarg. -## 0.14.4 +## 0.14.4 (2019-07-30) - [FIX] Fixed a regression in 0.14.3 that prevented a tzinfo argument of type string to be passed to the `get()` function. Functionality such as `arrow.get("2019072807", "YYYYMMDDHH", tzinfo="UTC")` should work as normal again. - [CHANGE] Moved `backports.functools_lru_cache` dependency from `extra_requires` to `install_requires` for `Python 2.7` installs to fix [#495](https://github.com/crsmithdev/arrow/issues/495). -## 0.14.3 +## 0.14.3 (2019-07-28) - [NEW] Added full support for Python 3.8. - [CHANGE] Added warnings for upcoming factory.get() parsing changes in 0.15.0. Please see [issue #612](https://github.com/crsmithdev/arrow/issues/612) for full details. @@ -61,16 +61,16 @@ arrow.get(1565358758.123413) - [FIX] factory.get() can now construct from kwargs. - [FIX] Added meridians to Spanish Locale. -## 0.14.2 +## 0.14.2 (2019-06-06) - [CHANGE] Travis CI builds now use tox to lint and run tests. - [FIX] Fixed UnicodeDecodeError on certain locales (#600). -## 0.14.1 +## 0.14.1 (2019-06-06) - [FIX] Fixed "ImportError: No module named 'dateutil'" (#598). -## 0.14.0 +## 0.14.0 (2019-06-06) - [NEW] Added provisional support for Python 3.8. - [CHANGE] Removed support for EOL Python 3.4. @@ -79,7 +79,7 @@ arrow.get(1565358758.123413) - [FIX] Enabled flake8 and black on travis builds. - [FIX] Formatted code using black and isort. -## 0.13.2 +## 0.13.2 (2019-05-30) - [NEW] Add is_between method. - [FIX] Improved humanize behaviour for near zero durations (#416). @@ -87,13 +87,13 @@ arrow.get(1565358758.123413) - [FIX] Documentation updates. - [FIX] Improvements to German Locale. -## 0.13.1 +## 0.13.1 (2019-02-17) - [NEW] Add support for Python 3.7. - [CHANGE] Remove deprecation decorators for Arrow.range(), Arrow.span_range() and Arrow.interval(), all now return generators, wrap with list() to get old behavior. - [FIX] Documentation and docstring updates. -## 0.13.0 +## 0.13.0 (2019-01-09) - [NEW] Added support for Python 3.6. - [CHANGE] Drop support for Python 2.6/3.3. From d79320b0e391f040bfd307f073db28eecd9fa9cb Mon Sep 17 00:00:00 2001 From: Pedrat Date: Thu, 12 Sep 2019 17:15:29 +0100 Subject: [PATCH 299/649] Adds {0} seconds to portuguese and brazilian portuguese locales, adds tests for Portuguese and BrazilianPortuguese locales --- arrow/locales.py | 6 ++++-- tests/locales_tests.py | 44 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 930b6b5b6..0be975d7a 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1777,7 +1777,8 @@ class PortugueseLocale(Locale): timeframes = { "now": "agora", - "seconds": "segundos", + "second": "um segundo", + "seconds": "{0} segundos", "minute": "um minuto", "minutes": "{0} minutos", "hour": "uma hora", @@ -1845,7 +1846,8 @@ class BrazilianPortugueseLocale(PortugueseLocale): timeframes = { "now": "agora", - "seconds": "segundos", + "second": "um segundo", + "seconds": "{0} segundos", "minute": "um minuto", "minutes": "{0} minutos", "hour": "uma hora", diff --git a/tests/locales_tests.py b/tests/locales_tests.py index ae68eea22..acc744e9e 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -836,3 +836,47 @@ def test_format_timeframe(self): self.assertEqual(self.locale._format_timeframe("year", -1), "üks aasta") self.assertEqual(self.locale._format_timeframe("years", -4), "4 aastat") self.assertEqual(self.locale._format_timeframe("years", -14), "14 aastat") + + +class PortugueseLocaleTests(Chai): + def setUp(self): + super(PortugueseLocaleTests, self).setUp() + + self.locale = locales.PortugueseLocale() + + def test_format_timeframe(self): + self.assertEqual(self.locale._format_timeframe("now", 0), "agora") + self.assertEqual(self.locale._format_timeframe("second", 1), "um segundo") + self.assertEqual(self.locale._format_timeframe("seconds", 30), "30 segundos") + self.assertEqual(self.locale._format_timeframe("minute", 1), "um minuto") + self.assertEqual(self.locale._format_timeframe("minutes", 40), "40 minutos") + self.assertEqual(self.locale._format_timeframe("hour", 1), "uma hora") + self.assertEqual(self.locale._format_timeframe("hours", 23), "23 horas") + self.assertEqual(self.locale._format_timeframe("day", 1), "um dia") + self.assertEqual(self.locale._format_timeframe("days", 12), "12 dias") + self.assertEqual(self.locale._format_timeframe("month", 1), "um mês") + self.assertEqual(self.locale._format_timeframe("months", 11), "11 meses") + self.assertEqual(self.locale._format_timeframe("year", 1), "um ano") + self.assertEqual(self.locale._format_timeframe("years", 12), "12 anos") + + +class BrazilianLocaleTests(Chai): + def setUp(self): + super(BrazilianLocaleTests, self).setUp() + + self.locale = locales.BrazilianPortugueseLocale() + + def test_format_timeframe(self): + self.assertEqual(self.locale._format_timeframe("now", 0), "agora") + self.assertEqual(self.locale._format_timeframe("second", 1), "um segundo") + self.assertEqual(self.locale._format_timeframe("seconds", 30), "30 segundos") + self.assertEqual(self.locale._format_timeframe("minute", 1), "um minuto") + self.assertEqual(self.locale._format_timeframe("minutes", 40), "40 minutos") + self.assertEqual(self.locale._format_timeframe("hour", 1), "uma hora") + self.assertEqual(self.locale._format_timeframe("hours", 23), "23 horas") + self.assertEqual(self.locale._format_timeframe("day", 1), "um dia") + self.assertEqual(self.locale._format_timeframe("days", 12), "12 dias") + self.assertEqual(self.locale._format_timeframe("month", 1), "um mês") + self.assertEqual(self.locale._format_timeframe("months", 11), "11 meses") + self.assertEqual(self.locale._format_timeframe("year", 1), "um ano") + self.assertEqual(self.locale._format_timeframe("years", 12), "12 anos") From dca32c3bbe9da0825f8a3edbf461349d14f16bee Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Fri, 13 Sep 2019 02:30:22 -0600 Subject: [PATCH 300/649] Added CHANGELOG to docs (#673) --- CHANGELOG.md => CHANGELOG.rst | 195 +++++++++++++++++++++------------- MANIFEST.in | 2 +- docs/index.rst | 10 +- docs/releases.rst | 3 + 4 files changed, 133 insertions(+), 77 deletions(-) rename CHANGELOG.md => CHANGELOG.rst (69%) create mode 100644 docs/releases.rst diff --git a/CHANGELOG.md b/CHANGELOG.rst similarity index 69% rename from CHANGELOG.md rename to CHANGELOG.rst index 9af6e4e46..1d77ec051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.rst @@ -1,77 +1,93 @@ -# Changelog +Changelog +========= -## 0.15.1 (2019-09-10) +0.15.1 (2019-09-10) +------------------- -- [NEW] Added `humanize` week granularity translations for Japanese. +- [NEW] Added ``humanize`` week granularity translations for Japanese. - [FIX] Fixed a bug that caused Arrow to fail when passed a negative timestamp string. -- [FIX] Fixed a bug that caused Arrow to fail when passed a datetime object with `tzinfo` of type `StaticTzInfo`. +- [FIX] Fixed a bug that caused Arrow to fail when passed a datetime object with ``tzinfo`` of type ``StaticTzInfo``. -## 0.15.0 (2019-09-08) -- [NEW] Added support for DDD and DDDD ordinal date tokens. The following functionality is now possible: `arrow.get("1998-045")`, `arrow.get("1998-45", "YYYY-DDD")`, `arrow.get("1998-045", "YYYY-DDDD")`. -- [NEW] ISO 8601 basic format for dates and times is now supported (e.g. `YYYYMMDDTHHmmssZ`). -- [NEW] Added `humanize` week granularity translations for French, Russian and Swiss German locales. -- [CHANGE] Timestamps of type `str` are no longer supported **without a format string** in the `arrow.get()` method. This change was made to support the ISO 8601 basic format and to address bugs such as [#447](https://github.com/crsmithdev/arrow/issues/447). +0.15.0 (2019-09-08) +------------------- -```python -# will NOT work in v0.15.0 -arrow.get("1565358758") -arrow.get("1565358758.123413") +- [NEW] Added support for DDD and DDDD ordinal date tokens. The following functionality is now possible: ``arrow.get("1998-045")``, ``arrow.get("1998-45", "YYYY-DDD")``, ``arrow.get("1998-045", "YYYY-DDDD")``. +- [NEW] ISO 8601 basic format for dates and times is now supported (e.g. ``YYYYMMDDTHHmmssZ``). +- [NEW] Added ``humanize`` week granularity translations for French, Russian and Swiss German locales. +- [CHANGE] Timestamps of type ``str`` are no longer supported **without a format string** in the ``arrow.get()`` method. This change was made to support the ISO 8601 basic format and to address bugs such as `#447 `_. -# will work in v0.15.0 -arrow.get("1565358758", "X") -arrow.get("1565358758.123413", "X") -arrow.get(1565358758) -arrow.get(1565358758.123413) -``` +The following will NOT work in v0.15.0: -- [CHANGE] When a meridian token (a|A) is passed and no meridians are available for the specified locale (e.g. unsupported or untranslated) a `ParserError` is raised. -- [CHANGE] The timestamp token (`X`) will now match float timestamps of type `str`: `arrow.get(“1565358758.123415”, “X”)`. -- [CHANGE] Strings with leading and/or trailing whitespace will no longer be parsed without a format string. Please see [the docs](https://arrow.readthedocs.io/en/latest/#regular-expressions) for ways to handle this. -- [FIX] The timestamp token (`X`) will now only match on strings that **strictly contain integers and floats**, preventing incorrect matches. -- [FIX] Most instances of `arrow.get()` returning an incorrect `Arrow` object from a partial parsing match have been eliminated. The following issue have been addressed: [#91](https://github.com/crsmithdev/arrow/issues/91), [#196](https://github.com/crsmithdev/arrow/issues/196), [#396](https://github.com/crsmithdev/arrow/issues/396), [#434](https://github.com/crsmithdev/arrow/issues/434), [#447](https://github.com/crsmithdev/arrow/issues/447), [#456](https://github.com/crsmithdev/arrow/issues/456), [#519](https://github.com/crsmithdev/arrow/issues/519), [#538](https://github.com/crsmithdev/arrow/issues/538), [#560](https://github.com/crsmithdev/arrow/issues/560). +.. code-block:: python -## 0.14.7 (2019-09-04) + >>> arrow.get("1565358758") + >>> arrow.get("1565358758.123413") -- [CHANGE] `ArrowParseWarning` will no longer be printed on every call to `arrow.get()` with a datetime string. The purpose of the warning was to start a conversation about the upcoming 0.15.0 changes and we appreciate all the feedback that the community has given us! +The following will work in v0.15.0: -## 0.14.6 (2019-08-28) +.. code-block:: python -- [NEW] Added support for `week` granularity in `Arrow.humanize()`. For example, `arrow.utcnow().shift(weeks=-1).humanize(granularity="week")` outputs "a week ago". This change introduced two new untranslated words, `week` and `weeks`, to all locale dictionaries, so locale contributions are welcome! + >>> arrow.get("1565358758", "X") + >>> arrow.get("1565358758.123413", "X") + >>> arrow.get(1565358758) + >>> arrow.get(1565358758.123413) + +- [CHANGE] When a meridian token (a|A) is passed and no meridians are available for the specified locale (e.g. unsupported or untranslated) a ``ParserError`` is raised. +- [CHANGE] The timestamp token (``X``) will now match float timestamps of type ``str``: ``arrow.get(“1565358758.123415”, “X”)``. +- [CHANGE] Strings with leading and/or trailing whitespace will no longer be parsed without a format string. Please see `the docs `_ for ways to handle this. +- [FIX] The timestamp token (``X``) will now only match on strings that **strictly contain integers and floats**, preventing incorrect matches. +- [FIX] Most instances of ``arrow.get()`` returning an incorrect ``Arrow`` object from a partial parsing match have been eliminated. The following issue have been addressed: `#91 `_, `#196 `_, `#396 `_, `#434 `_, `#447 `_, `#456 `_, `#519 `_, `#538 `_, `#560 `_. + +0.14.7 (2019-09-04) +------------------- + +- [CHANGE] ``ArrowParseWarning`` will no longer be printed on every call to ``arrow.get()`` with a datetime string. The purpose of the warning was to start a conversation about the upcoming 0.15.0 changes and we appreciate all the feedback that the community has given us! + +0.14.6 (2019-08-28) +------------------- + +- [NEW] Added support for ``week`` granularity in ``Arrow.humanize()``. For example, ``arrow.utcnow().shift(weeks=-1).humanize(granularity="week")`` outputs "a week ago". This change introduced two new untranslated words, ``week`` and ``weeks``, to all locale dictionaries, so locale contributions are welcome! - [NEW] Fully translated the Brazilian Portugese locale. - [CHANGE] Updated the Macedonian locale to inherit from a Slavic base. -- [FIX] Fixed a bug that caused `arrow.get()` to ignore tzinfo arguments of type string (e.g. `arrow.get(tzinfo="Europe/Paris")`). -- [FIX] Fixed a bug that occurred when `arrow.Arrow()` was instantiated with a `pytz` tzinfo object. -- [FIX] Fixed a bug that caused Arrow to fail when passed a sub-second token, that when rounded, had a value greater than 999999 (e.g. `arrow.get("2015-01-12T01:13:15.9999995")`). Arrow should now accurately propagate the rounding for large sub-second tokens. +- [FIX] Fixed a bug that caused ``arrow.get()`` to ignore tzinfo arguments of type string (e.g. ``arrow.get(tzinfo="Europe/Paris")``). +- [FIX] Fixed a bug that occurred when ``arrow.Arrow()`` was instantiated with a ``pytz`` tzinfo object. +- [FIX] Fixed a bug that caused Arrow to fail when passed a sub-second token, that when rounded, had a value greater than 999999 (e.g. ``arrow.get("2015-01-12T01:13:15.9999995")``). Arrow should now accurately propagate the rounding for large sub-second tokens. -## 0.14.5 (2019-08-09) +0.14.5 (2019-08-09) +------------------- - [NEW] Added Afrikaans locale. -- [CHANGE] Removed deprecated `replace` shift functionality. Users looking to pass plural properties to the `replace` function to shift values should use `shift` instead. -- [FIX] Fixed bug that occurred when `factory.get()` was passed a locale kwarg. +- [CHANGE] Removed deprecated ``replace`` shift functionality. Users looking to pass plural properties to the ``replace`` function to shift values should use ``shift`` instead. +- [FIX] Fixed bug that occurred when ``factory.get()`` was passed a locale kwarg. -## 0.14.4 (2019-07-30) +0.14.4 (2019-07-30) +------------------- -- [FIX] Fixed a regression in 0.14.3 that prevented a tzinfo argument of type string to be passed to the `get()` function. Functionality such as `arrow.get("2019072807", "YYYYMMDDHH", tzinfo="UTC")` should work as normal again. -- [CHANGE] Moved `backports.functools_lru_cache` dependency from `extra_requires` to `install_requires` for `Python 2.7` installs to fix [#495](https://github.com/crsmithdev/arrow/issues/495). +- [FIX] Fixed a regression in 0.14.3 that prevented a tzinfo argument of type string to be passed to the ``get()`` function. Functionality such as ``arrow.get("2019072807", "YYYYMMDDHH", tzinfo="UTC")`` should work as normal again. +- [CHANGE] Moved ``backports.functools_lru_cache`` dependency from ``extra_requires`` to ``install_requires`` for ``Python 2.7`` installs to fix `#495 `_. -## 0.14.3 (2019-07-28) +0.14.3 (2019-07-28) +------------------- - [NEW] Added full support for Python 3.8. -- [CHANGE] Added warnings for upcoming factory.get() parsing changes in 0.15.0. Please see [issue #612](https://github.com/crsmithdev/arrow/issues/612) for full details. +- [CHANGE] Added warnings for upcoming factory.get() parsing changes in 0.15.0. Please see `#612 `_ for full details. - [FIX] Extensive refactor and update of documentation. - [FIX] factory.get() can now construct from kwargs. - [FIX] Added meridians to Spanish Locale. -## 0.14.2 (2019-06-06) +0.14.2 (2019-06-06) +------------------- - [CHANGE] Travis CI builds now use tox to lint and run tests. - [FIX] Fixed UnicodeDecodeError on certain locales (#600). -## 0.14.1 (2019-06-06) +0.14.1 (2019-06-06) +------------------- -- [FIX] Fixed "ImportError: No module named 'dateutil'" (#598). +- [FIX] Fixed ``ImportError: No module named 'dateutil'`` (#598). -## 0.14.0 (2019-06-06) +0.14.0 (2019-06-06) +------------------- - [NEW] Added provisional support for Python 3.8. - [CHANGE] Removed support for EOL Python 3.4. @@ -80,7 +96,8 @@ arrow.get(1565358758.123413) - [FIX] Enabled flake8 and black on travis builds. - [FIX] Formatted code using black and isort. -## 0.13.2 (2019-05-30) +0.13.2 (2019-05-30) +------------------- - [NEW] Add is_between method. - [FIX] Improved humanize behaviour for near zero durations (#416). @@ -88,13 +105,15 @@ arrow.get(1565358758.123413) - [FIX] Documentation updates. - [FIX] Improvements to German Locale. -## 0.13.1 (2019-02-17) +0.13.1 (2019-02-17) +------------------- - [NEW] Add support for Python 3.7. - [CHANGE] Remove deprecation decorators for Arrow.range(), Arrow.span_range() and Arrow.interval(), all now return generators, wrap with list() to get old behavior. - [FIX] Documentation and docstring updates. -## 0.13.0 (2019-01-09) +0.13.0 (2019-01-09) +------------------- - [NEW] Added support for Python 3.6. - [CHANGE] Drop support for Python 2.6/3.3. @@ -111,16 +130,19 @@ arrow.get(1565358758.123413) - [FIX] Improve docs for get, now and utcnow methods. - [FIX] Correct typo in depreciation warning. -## 0.12.1 +0.12.1 +------ - [FIX] Allow universal wheels to be generated and reliably installed. - [FIX] Make humanize respect only_distance when granularity argument is also given. -## 0.12.0 +0.12.0 +------ - [FIX] Compatibility fix for Python 2.x -## 0.11.0 +0.11.0 +------ - [FIX] Fix grammar of ArabicLocale - [NEW] Add Nepali Locale @@ -130,13 +152,15 @@ arrow.get(1565358758.123413) - [FIX] Remove pip --user-mirrors flag - [NEW] Add Indonesian Locale -## 0.10.0 +0.10.0 +------ - [FIX] Fix getattr off by one for quarter - [FIX] Fix negative offset for UTC - [FIX] Update arrow.py -## 0.9.0 +0.9.0 +----- - [NEW] Remove duplicate code - [NEW] Support gnu date iso 8601 @@ -151,18 +175,21 @@ arrow.get(1565358758.123413) - [NEW] Azerbaijani locale added, locale issue fixed in Turkish. - [FIX] Format ParserError's raise message -## 0.8.0 +0.8.0 +----- - [] -## 0.7.1 +0.7.1 +----- - [NEW] Esperanto locale (batisteo) -## 0.7.0 +0.7.0 +----- - [FIX] Parse localized strings #228 (swistakm) -- [FIX] Modify tzinfo parameter in `get` api #221 (bottleimp) +- [FIX] Modify tzinfo parameter in ``get`` api #221 (bottleimp) - [FIX] Fix Czech locale (PrehistoricTeam) - [FIX] Raise TypeError when adding/subtracting non-dates (itsmeolivia) - [FIX] Fix pytz conversion error (Kudo) @@ -173,14 +200,15 @@ arrow.get(1565358758.123413) - [NEW] Search date in strings (beenje) - [NEW] Note that arrow's tokens differ from strptime's. (offby1) -## 0.6.0 +0.6.0 +----- - [FIX] Added support for Python 3 - [FIX] Avoid truncating oversized epoch timestamps. Fixes #216. - [FIX] Fixed month abbreviations for Ukrainian - [FIX] Fix typo timezone - [FIX] A couple of dialect fixes and two new languages -- [FIX] Spanish locale: `Miercoles` should have acute accent +- [FIX] Spanish locale: ``Miercoles`` should have acute accent - [Fix] Fix Finnish grammar - [FIX] Fix typo in 'Arrow.floor' docstring - [FIX] Use read() utility to open README @@ -190,7 +218,8 @@ arrow.get(1565358758.123413) - [NEW] Add count argument to span method - [NEW] Improved docs -## 0.5.1 - 0.5.4 +0.5.1 - 0.5.4 +------------- - [FIX] test the behavior of simplejson instead of calling for_json directly (tonyseek) - [FIX] Add Hebrew Locale (doodyparizada) @@ -198,7 +227,8 @@ arrow.get(1565358758.123413) - [FIX] Update setup.py Development Status level (andrewelkins) - [FIX] Case insensitive month match (cshowe) -## 0.5.0 +0.5.0 +----- - [NEW] struct_time addition. (mhworth) - [NEW] Version grep (eirnym) @@ -212,7 +242,8 @@ arrow.get(1565358758.123413) - [FIX] Parse lower-cased 'h' (tamentis) - [FIX] Slight modifications to Dutch locale (nvie) -## 0.4.4 +0.4.4 +----- - [NEW] Include the docs in the released tarball - [NEW] Czech localization Czech localization for Arrow @@ -230,7 +261,8 @@ arrow.get(1565358758.123413) - [FIX] In Norwegian and new Norwegian months and weekdays should not be capitalized - [FIX] Fixed discrepancy between specifying 'X' to arrow.get and specifying no template -## 0.4.3 +0.4.3 +----- - [NEW] Turkish locale (Emre) - [NEW] Arabic locale (Mosab Ahmad) @@ -242,7 +274,7 @@ arrow.get(1565358758.123413) - [NEW] Portuguese locale (Danielcorreia) - [NEW] ``h`` and ``hh`` strings are now supported (Averyonghub) - [FIX] An incorrect inflection in the Polish locale has been fixed (Avalanchy) -- [FIX] ``arrow.get`` now properly handles ``Date``s (Jaapz) +- [FIX] ``arrow.get`` now properly handles ``Date`` (Jaapz) - [FIX] Tests are now declared in ``setup.py`` and the manifest (Pypingou) - [FIX] ``__version__`` has been added to ``__init__.py`` (Sametmax) - [FIX] ISO 8601 strings can be parsed without a separator (Ivandiguisto / Root) @@ -251,7 +283,8 @@ arrow.get(1565358758.123413) - [FIX] Error messages for parse errors are now more descriptive (Maciej Albin) - [FIX] The parser now correctly checks for separators in strings (Mschwager) -## 0.4.2 +0.4.2 +----- - [NEW] Factory ``get`` method now accepts a single ``Arrow`` argument. - [NEW] Tokens SSSS, SSSSS and SSSSSS are supported in parsing. @@ -265,7 +298,8 @@ arrow.get(1565358758.123413) - [FIX] ``humanize`` now correctly returns unicode (Shvechikov) - [FIX] ``Arrow`` objects now pickle / unpickle correctly (Yoloseem) -## 0.4.1 +0.4.1 +----- - [NEW] Table / explanation of formatting & parsing tokens in docs - [NEW] Brazilian locale (Augusto2112) @@ -277,7 +311,8 @@ arrow.get(1565358758.123413) - [FIX] Factory ``get`` method should now handle unicode strings correctly (Bwells) - [FIX] Midnight and noon should now parse and format correctly (Bwells) -## 0.4.0 +0.4.0 +----- - [NEW] Format-free ISO-8601 parsing in factory ``get`` method - [NEW] Support for 'week' / 'weeks' in ``span``, ``range``, ``span_range``, ``floor`` and ``ceil`` @@ -292,18 +327,21 @@ arrow.get(1565358758.123413) - [CHANGE] Dropped timestamp support in ``range`` and ``span_range`` (never worked correctly) - [CHANGE] Dropped parsing of single string as tz string in factory ``get`` method (replaced by ISO-8601) -## 0.3.5 +0.3.5 +----- - [NEW] French locale (Cynddl) - [NEW] Spanish locale (Slapresta) - [FIX] Ranges handle multiple timezones correctly (Ftobia) -## 0.3.4 +0.3.4 +----- - [FIX] Humanize no longer sometimes returns the wrong month delta - [FIX] ``__format__`` works correctly with no format string -## 0.3.3 +0.3.3 +----- - [NEW] Python 2.6 support - [NEW] Initial support for locale-based parsing and formatting @@ -311,15 +349,18 @@ arrow.get(1565358758.123413) - [NEW] ``factory`` api method to obtain a factory for a custom type - [FIX] Python 3 support and tests completely ironed out -## 0.3.2 +0.3.2 +----- - [NEW] Python 3+ support -## 0.3.1 +0.3.1 +----- - [FIX] The old ``arrow`` module function handles timestamps correctly as it used to -## 0.3.0 +0.3.0 +----- - [NEW] ``Arrow.replace`` method - [NEW] Accept timestamps, datetimes and Arrows for datetime inputs, where reasonable @@ -328,12 +369,14 @@ arrow.get(1565358758.123413) - [CHANGE] Plural attribute name semantics altered: single -> absolute, plural -> relative - [CHANGE] Plural names no longer supported as properties (e.g. ``arrow.utcnow().years``) -## 0.2.1 +0.2.1 +----- - [NEW] Support for localized humanization - [NEW] English, Russian, Greek, Korean, Chinese locales -## 0.2.0 +0.2.0 +----- - **REWRITE** - [NEW] Date parsing @@ -343,14 +386,16 @@ arrow.get(1565358758.123413) - [NEW] ``clone`` method - [NEW] ``get``, ``now`` and ``utcnow`` API methods -## 0.1.6 +0.1.6 +----- - [NEW] Humanized time deltas - [NEW] ``__eq__`` implemented - [FIX] Issues with conversions related to daylight savings time resolved - [CHANGE] ``__str__`` uses ISO formatting -## 0.1.5 +0.1.5 +----- - **Started tracking changes** - [NEW] Parsing of ISO-formatted time zone offsets (e.g. '+02:30', '-05:00') diff --git a/MANIFEST.in b/MANIFEST.in index 3eafcd8f7..51902173e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include LICENSE CHANGELOG.md README.rst +include LICENSE CHANGELOG.rst README.rst recursive-include tests *.py recursive-include docs *.py *.rst *.bat Makefile diff --git a/docs/index.rst b/docs/index.rst index 457e59f2a..f6c9ff7e4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ Arrow: Better dates & times for Python ====================================== -Release v\ |release|. (`Installation`_) (`Changelog `_) +Release v\ |release|. (`Installation`_) (`Changelog `_) .. include:: ../README.rst :start-after: start-inclusion-marker-do-not-remove @@ -446,3 +446,11 @@ arrow.locale .. automodule:: arrow.locales :members: :undoc-members: + +Release History +--------------- + +.. toctree:: + :maxdepth: 2 + + releases diff --git a/docs/releases.rst b/docs/releases.rst new file mode 100644 index 000000000..22e1e59c8 --- /dev/null +++ b/docs/releases.rst @@ -0,0 +1,3 @@ +.. _releases: + +.. include:: ../CHANGELOG.rst From 67852370e5f6b0984390e11b0e030d6e019c4210 Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Sat, 14 Sep 2019 11:21:33 +0100 Subject: [PATCH 301/649] Bump version to 0.15.2 and update changelog (#674) --- CHANGELOG.rst | 7 +++++++ arrow/_version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1d77ec051..2b62ab3a0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Changelog ========= +0.15.2 (2019-09-14) +------------------- + +- [NEW] Added ``humanize`` week granularity translations for Portuguese and Brazilian Portuguese. +- [NEW] Embedded changelog within docs and added release dates to versions. +- [FIX] Fixed a bug that caused test failures on Windows only, see `#668 `_ for details. + 0.15.1 (2019-09-10) ------------------- diff --git a/arrow/_version.py b/arrow/_version.py index 903e77ce1..c0d499920 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.15.1" +__version__ = "0.15.2" From bba2b315abb3d694f4687391f9219b0c2b2948ab Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Sat, 7 Sep 2019 19:30:20 +0100 Subject: [PATCH 302/649] Initial work to allow arrow to create from a isocalendar tuple --- arrow/factory.py | 6 +++++- arrow/util.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/arrow/factory.py b/arrow/factory.py index f3f3b7fe6..456ed1e8a 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -17,7 +17,7 @@ from arrow import parser from arrow.arrow import Arrow -from arrow.util import is_timestamp, isstr +from arrow.util import is_timestamp, iso_to_gregorian, isstr class ArrowFactory(object): @@ -179,6 +179,10 @@ def get(self, *args, **kwargs): if isinstance(arg, date): return self.type.fromdate(arg) + if isinstance(arg, tuple): + dt = iso_to_gregorian(*arg) + return self.type.fromdate(dt) + # (tzinfo) -> now, @ tzinfo. elif isinstance(arg, dt_tzinfo): return self.type.now(arg) diff --git a/arrow/util.py b/arrow/util.py index 8a379a4ef..981a80072 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -2,6 +2,9 @@ from __future__ import absolute_import from datetime import datetime +import datetime +import sys +import warnings def total_seconds(td): # pragma: no cover @@ -18,6 +21,17 @@ def is_timestamp(value): return False +# https://stackoverflow.com/a/1700069 +def iso_to_gregorian(iso_year, iso_week, iso_day): + "The gregorian calendar date of the first day of the given ISO year" + "Gregorian calendar date for the given ISO year, week and day" + fourth_jan = datetime.date(iso_year, 1, 4) + delta = datetime.timedelta(fourth_jan.isoweekday() - 1) + year_start = fourth_jan - delta + # year_start = iso_year_start(iso_year) + return year_start + datetime.timedelta(days=iso_day - 1, weeks=iso_week - 1) + + # Python 2.7 / 3.0+ definitions for isstr function. try: # pragma: no cover From 60c049970d94c66ac06abfb4a1f9fe6420343bb1 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Tue, 10 Sep 2019 17:52:16 +0100 Subject: [PATCH 303/649] Further work on isotuples --- arrow/factory.py | 13 ++++++++++--- arrow/util.py | 16 ++++++++-------- tests/factory_tests.py | 9 +++++++++ 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/arrow/factory.py b/arrow/factory.py index 456ed1e8a..291ef1c9f 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -179,9 +179,10 @@ def get(self, *args, **kwargs): if isinstance(arg, date): return self.type.fromdate(arg) - if isinstance(arg, tuple): - dt = iso_to_gregorian(*arg) - return self.type.fromdate(dt) + # # (iso calendar) -> convert then from date + # if isinstance(arg, tuple): + # dt = iso_to_gregorian(*arg) + # return self.type.fromdate(dt) # (tzinfo) -> now, @ tzinfo. elif isinstance(arg, dt_tzinfo): @@ -196,6 +197,12 @@ def get(self, *args, **kwargs): elif isinstance(arg, struct_time): return self.type.utcfromtimestamp(calendar.timegm(arg)) + # (iso calendar) -> convert then from date + elif isinstance(arg, tuple): + # TODO check len is 3 exactly + dt = iso_to_gregorian(*arg) + return self.type.fromdate(dt) + else: raise TypeError( "Can't parse single argument type of '{}'".format(type(arg)) diff --git a/arrow/util.py b/arrow/util.py index 981a80072..150ce1734 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -from datetime import datetime +# from datetime import datetime import datetime -import sys -import warnings def total_seconds(td): # pragma: no cover @@ -15,21 +13,23 @@ def is_timestamp(value): if isinstance(value, bool): return False try: - datetime.fromtimestamp(value) + datetime.datetime.fromtimestamp(value) return True except TypeError: return False -# https://stackoverflow.com/a/1700069 +# Credit to https://stackoverflow.com/a/1700069 def iso_to_gregorian(iso_year, iso_week, iso_day): + """Converts an iso weekday tuple into a datetime object.""" "The gregorian calendar date of the first day of the given ISO year" "Gregorian calendar date for the given ISO year, week and day" fourth_jan = datetime.date(iso_year, 1, 4) delta = datetime.timedelta(fourth_jan.isoweekday() - 1) year_start = fourth_jan - delta - # year_start = iso_year_start(iso_year) - return year_start + datetime.timedelta(days=iso_day - 1, weeks=iso_week - 1) + gregorian = year_start + datetime.timedelta(days=iso_day - 1, weeks=iso_week - 1) + + return gregorian # Python 2.7 / 3.0+ definitions for isstr function. @@ -47,4 +47,4 @@ def isstr(s): return isinstance(s, str) -__all__ = ["total_seconds", "is_timestamp", "isstr"] +__all__ = ["total_seconds", "is_timestamp", "isstr", "iso_to_gregorian"] diff --git a/tests/factory_tests.py b/tests/factory_tests.py index bccc16376..5ea2d662f 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -140,6 +140,15 @@ def test_one_arg_iso_str(self): assertDtEqual(self.factory.get(dt.isoformat()), dt.replace(tzinfo=tz.tzutc())) + def test_one_arg_iso_calendar(self): + + dt = datetime(2004, 1, 4) + + self.expected = self.factory.get(dt) + + assertEqual(self.factory.get(dt.isocalendar()), self.expected) + # TODO way more tests 2,4 element tuples, other dates, week 55 etc etc + def test_one_arg_other(self): with self.assertRaises(TypeError): From da09a84c76e6571fc3995ec9fc44dedb4e8daa64 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Wed, 11 Sep 2019 20:46:24 +0100 Subject: [PATCH 304/649] Add test cases --- arrow/factory.py | 7 +------ arrow/util.py | 3 +-- tests/factory_tests.py | 16 ++++++++++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/arrow/factory.py b/arrow/factory.py index 291ef1c9f..6123472bc 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -179,11 +179,6 @@ def get(self, *args, **kwargs): if isinstance(arg, date): return self.type.fromdate(arg) - # # (iso calendar) -> convert then from date - # if isinstance(arg, tuple): - # dt = iso_to_gregorian(*arg) - # return self.type.fromdate(dt) - # (tzinfo) -> now, @ tzinfo. elif isinstance(arg, dt_tzinfo): return self.type.now(arg) @@ -199,7 +194,7 @@ def get(self, *args, **kwargs): # (iso calendar) -> convert then from date elif isinstance(arg, tuple): - # TODO check len is 3 exactly + # TODO check len is 3 exactly or let function handle it? dt = iso_to_gregorian(*arg) return self.type.fromdate(dt) diff --git a/arrow/util.py b/arrow/util.py index 150ce1734..ab1f9b99d 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -22,8 +22,7 @@ def is_timestamp(value): # Credit to https://stackoverflow.com/a/1700069 def iso_to_gregorian(iso_year, iso_week, iso_day): """Converts an iso weekday tuple into a datetime object.""" - "The gregorian calendar date of the first day of the given ISO year" - "Gregorian calendar date for the given ISO year, week and day" + fourth_jan = datetime.date(iso_year, 1, 4) delta = datetime.timedelta(fourth_jan.isoweekday() - 1) year_start = fourth_jan - delta diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 5ea2d662f..ccd31a35f 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -142,12 +142,20 @@ def test_one_arg_iso_str(self): def test_one_arg_iso_calendar(self): - dt = datetime(2004, 1, 4) + pairs = [(datetime(2004, 1, 4), (2004, 1, 7)), + (datetime(2008, 12, 30), (2009, 1, 2)), + (datetime(2010, 1, 2), (2009, 53, 6)), + (datetime(2000, 2, 29), (2000, 9, 2))] - self.expected = self.factory.get(dt) + for pair in pairs: + dt, iso = pair + assertEqual(self.factory.get(iso), self.factory.get(dt)) - assertEqual(self.factory.get(dt.isocalendar()), self.expected) - # TODO way more tests 2,4 element tuples, other dates, week 55 etc etc + with self.assertRaises(TypeError): + self.factory.get((2014, 7, 1, 4)) + + with self.assertRaises(TypeError): + self.factory.get((2014, 7)) def test_one_arg_other(self): From cc31f513226d0912aa5cc84d7622b60fabad2392 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Thu, 12 Sep 2019 17:35:53 +0100 Subject: [PATCH 305/649] Update docstring --- arrow/factory.py | 15 ++++++++++----- arrow/util.py | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/arrow/factory.py b/arrow/factory.py index 6123472bc..e00fc51db 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -99,6 +99,16 @@ def get(self, *args, **kwargs): >>> arrow.get(date(2013, 5, 5)) + **One** time.struct time:: + + >>> arrow.get(gmtime(0)) + + + **One** iso calendar ``tuple``, to get that week date in UTC:: + + >>> arrow.get((2013, 18, 7)) + + **Two** arguments, a naive or aware ``datetime``, and a replacement :ref:`timezone expression `:: @@ -126,11 +136,6 @@ def get(self, *args, **kwargs): >>> arrow.get(2013, 5, 5, 12, 30, 45) - **One** time.struct time:: - - >>> arrow.get(gmtime(0)) - - """ arg_count = len(args) diff --git a/arrow/util.py b/arrow/util.py index ab1f9b99d..b087f41b8 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -# from datetime import datetime import datetime @@ -23,6 +22,7 @@ def is_timestamp(value): def iso_to_gregorian(iso_year, iso_week, iso_day): """Converts an iso weekday tuple into a datetime object.""" + # The first week of the year always contains 4 Jan. fourth_jan = datetime.date(iso_year, 1, 4) delta = datetime.timedelta(fourth_jan.isoweekday() - 1) year_start = fourth_jan - delta From 777e70695a10bef2713bd8ce100f11a372296d82 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Thu, 12 Sep 2019 17:44:50 +0100 Subject: [PATCH 306/649] Fix lint --- tests/factory_tests.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/factory_tests.py b/tests/factory_tests.py index ccd31a35f..48d8e794e 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -142,10 +142,12 @@ 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)), - (datetime(2010, 1, 2), (2009, 53, 6)), - (datetime(2000, 2, 29), (2000, 9, 2))] + pairs = [ + (datetime(2004, 1, 4), (2004, 1, 7)), + (datetime(2008, 12, 30), (2009, 1, 2)), + (datetime(2010, 1, 2), (2009, 53, 6)), + (datetime(2000, 2, 29), (2000, 9, 2)), + ] for pair in pairs: dt, iso = pair From 324eeea0fad4a46bf9bb8534783f8ce4d186e207 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Thu, 12 Sep 2019 17:53:57 +0100 Subject: [PATCH 307/649] Fix flake8 --- tests/factory_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 48d8e794e..9edcdc0a6 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -151,7 +151,7 @@ def test_one_arg_iso_calendar(self): for pair in pairs: dt, iso = pair - assertEqual(self.factory.get(iso), self.factory.get(dt)) + self.assertEqual(self.factory.get(iso), self.factory.get(dt)) with self.assertRaises(TypeError): self.factory.get((2014, 7, 1, 4)) From 559e010ca046a741a758302896ce58ad8a7dd40f Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Sun, 15 Sep 2019 17:35:23 +0100 Subject: [PATCH 308/649] Error handling and extra tests --- arrow/factory.py | 1 - arrow/util.py | 5 +++++ tests/util_tests.py | 7 +++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/arrow/factory.py b/arrow/factory.py index e00fc51db..e112d5155 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -199,7 +199,6 @@ def get(self, *args, **kwargs): # (iso calendar) -> convert then from date elif isinstance(arg, tuple): - # TODO check len is 3 exactly or let function handle it? dt = iso_to_gregorian(*arg) return self.type.fromdate(dt) diff --git a/arrow/util.py b/arrow/util.py index b087f41b8..31597cf21 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -22,6 +22,11 @@ def is_timestamp(value): def iso_to_gregorian(iso_year, iso_week, iso_day): """Converts an iso weekday tuple into a datetime object.""" + if iso_week == 0 or iso_day == 0: + raise ValueError( + "Iso Calendar week and day values must be 1-53 and 1-7 respectively." + ) + # The first week of the year always contains 4 Jan. fourth_jan = datetime.date(iso_year, 1, 4) delta = datetime.timedelta(fourth_jan.isoweekday() - 1) diff --git a/tests/util_tests.py b/tests/util_tests.py index 721697732..21e98c7c1 100644 --- a/tests/util_tests.py +++ b/tests/util_tests.py @@ -29,3 +29,10 @@ def test_is_timestamp(self): overflow_timestamp_int = int(overflow_timestamp_float) with self.assertRaises((OverflowError, ValueError)): util.is_timestamp(overflow_timestamp_int) + + def test_iso_gregorian(self): + with self.assertRaises(ValueError): + util.iso_to_gregorian(2013, 0, 5) + + with self.assertRaises(ValueError): + util.iso_to_gregorian(2013, 8, 0) From 06d111330caa373e5fc14ae91ae6b4b61d99fa99 Mon Sep 17 00:00:00 2001 From: Argenis Fontalvo Date: Mon, 16 Sep 2019 11:41:26 -0300 Subject: [PATCH 309/649] i18n(locales): add week granularity localization for Spanish --- arrow/locales.py | 2 ++ tests/locales_tests.py | 48 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 0be975d7a..c27a11e9f 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -374,6 +374,8 @@ class SpanishLocale(Locale): "hours": "{0} horas", "day": "un día", "days": "{0} días", + "week": "una semana", + "weeks": "{0} semanas", "month": "un mes", "months": "{0} meses", "year": "un año", diff --git a/tests/locales_tests.py b/tests/locales_tests.py index acc744e9e..86f6f5f78 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -150,6 +150,54 @@ def test_ordinal_number(self): self.assertEqual(locale.ordinal_number(1), "1º") + def test_format_timeframe(self): + locale = locales.SpanishLocale() + self.assertEqual(locale._format_timeframe("now", 0), "ahora") + self.assertEqual(locale._format_timeframe("seconds", 1), "segundos") + self.assertEqual(locale._format_timeframe("seconds", 3), "segundos") + self.assertEqual(locale._format_timeframe("seconds", 30), "segundos") + self.assertEqual(locale._format_timeframe("minute", 1), "un minuto") + self.assertEqual(locale._format_timeframe("minutes", 4), "4 minutos") + self.assertEqual(locale._format_timeframe("minutes", 40), "40 minutos") + self.assertEqual(locale._format_timeframe("hour", 1), "una hora") + self.assertEqual(locale._format_timeframe("hours", 5), "5 horas") + self.assertEqual(locale._format_timeframe("hours", 23), "23 horas") + self.assertEqual(locale._format_timeframe("day", 1), "un día") + self.assertEqual(locale._format_timeframe("days", 6), "6 días") + self.assertEqual(locale._format_timeframe("days", 12), "12 días") + self.assertEqual(locale._format_timeframe("week", 1), "una semana") + self.assertEqual(locale._format_timeframe("weeks", 2), "2 semanas") + self.assertEqual(locale._format_timeframe("weeks", 3), "3 semanas") + self.assertEqual(locale._format_timeframe("month", 1), "un mes") + self.assertEqual(locale._format_timeframe("months", 7), "7 meses") + self.assertEqual(locale._format_timeframe("months", 11), "11 meses") + self.assertEqual(locale._format_timeframe("year", 1), "un año") + self.assertEqual(locale._format_timeframe("years", 8), "8 años") + self.assertEqual(locale._format_timeframe("years", 12), "12 años") + + self.assertEqual(locale._format_timeframe("now", 0), "ahora") + self.assertEqual(locale._format_timeframe("seconds", -1), "segundos") + self.assertEqual(locale._format_timeframe("seconds", -9), "segundos") + self.assertEqual(locale._format_timeframe("seconds", -12), "segundos") + self.assertEqual(locale._format_timeframe("minute", -1), "un minuto") + self.assertEqual(locale._format_timeframe("minutes", -2), "2 minutos") + self.assertEqual(locale._format_timeframe("minutes", -10), "10 minutos") + self.assertEqual(locale._format_timeframe("hour", -1), "una hora") + self.assertEqual(locale._format_timeframe("hours", -3), "3 horas") + self.assertEqual(locale._format_timeframe("hours", -11), "11 horas") + self.assertEqual(locale._format_timeframe("day", -1), "un día") + self.assertEqual(locale._format_timeframe("days", -2), "2 días") + self.assertEqual(locale._format_timeframe("days", -12), "12 días") + self.assertEqual(locale._format_timeframe("week", -1), "una semana") + self.assertEqual(locale._format_timeframe("weeks", -2), "2 semanas") + self.assertEqual(locale._format_timeframe("weeks", -3), "3 semanas") + self.assertEqual(locale._format_timeframe("month", -1), "un mes") + self.assertEqual(locale._format_timeframe("months", -3), "3 meses") + self.assertEqual(locale._format_timeframe("months", -13), "13 meses") + self.assertEqual(locale._format_timeframe("year", -1), "un año") + self.assertEqual(locale._format_timeframe("years", -4), "4 años") + self.assertEqual(locale._format_timeframe("years", -14), "14 años") + class FrenchLocalesTests(Chai): def test_ordinal_number(self): From cbd99ba4c6b1dcd98ed5ee4d64f5fc06435f5198 Mon Sep 17 00:00:00 2001 From: Hopeweb Date: Tue, 17 Sep 2019 02:41:07 +0800 Subject: [PATCH 310/649] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E5=91=A8=EF=BC=8Cadd=20chinese=20week?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- arrow/locales.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index c27a11e9f..1cf17e440 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -805,6 +805,8 @@ class ChineseCNLocale(Locale): "hours": "{0}小时", "day": "1天", "days": "{0}天", + "week": "一周", + "weeks": "{0}周", "month": "1个月", "months": "{0}个月", "year": "1年", From 35018f9f33d78482b63f3e727facaa0dc093ef86 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 22 Sep 2019 06:11:12 -0400 Subject: [PATCH 311/649] Changed MANIFEST.in to MANIFEST and updated LICENSE text to match choosealicense.com (#679) --- .gitignore | 1 - LICENSE | 190 +++++++++++++++++++++++++++++++++++++++- MANIFEST.in => MANIFEST | 0 3 files changed, 189 insertions(+), 2 deletions(-) rename MANIFEST.in => MANIFEST (100%) diff --git a/.gitignore b/.gitignore index c68a2fbd9..0448d0cf0 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,6 @@ share/python-wheels/ *.egg-info/ .installed.cfg *.egg -MANIFEST # PyInstaller # Usually these files are written by a python script from a template diff --git a/LICENSE b/LICENSE index 727ded838..2bef500de 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,192 @@ -Copyright 2013 Chris Smith + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 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/MANIFEST.in b/MANIFEST similarity index 100% rename from MANIFEST.in rename to MANIFEST From 1babb3753a77e7eb53ec950931d873db93770e7c Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Tue, 24 Sep 2019 14:52:40 +0100 Subject: [PATCH 312/649] Check inputs and add edge cases to tests --- arrow/util.py | 9 +++++---- tests/factory_tests.py | 9 +++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/arrow/util.py b/arrow/util.py index 31597cf21..ed6777016 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -22,10 +22,11 @@ def is_timestamp(value): def iso_to_gregorian(iso_year, iso_week, iso_day): """Converts an iso weekday tuple into a datetime object.""" - if iso_week == 0 or iso_day == 0: - raise ValueError( - "Iso Calendar week and day values must be 1-53 and 1-7 respectively." - ) + if not 1 <= iso_week <= 53: + raise ValueError("Iso Calendar week value must be between 1-53.") + + if not 1 <= iso_day <= 7: + raise ValueError("Iso Calendar day value must be between 1-7") # The first week of the year always contains 4 Jan. fourth_jan = datetime.date(iso_year, 1, 4) diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 9edcdc0a6..8c00559ab 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -147,6 +147,9 @@ def test_one_arg_iso_calendar(self): (datetime(2008, 12, 30), (2009, 1, 2)), (datetime(2010, 1, 2), (2009, 53, 6)), (datetime(2000, 2, 29), (2000, 9, 2)), + (datetime(2005, 1, 1), (2004, 53, 6)), + (datetime(2010, 1, 4), (2010, 1, 1)), + (datetime(2010, 1, 3), (2009, 53, 7)), ] for pair in pairs: @@ -159,6 +162,12 @@ def test_one_arg_iso_calendar(self): with self.assertRaises(TypeError): self.factory.get((2014, 7)) + with self.assertRaises(ValueError): + self.factory.get((2014, 70, 1)) + + with self.assertRaises(ValueError): + self.factory.get((2014, 7, 10)) + def test_one_arg_other(self): with self.assertRaises(TypeError): From d7c81700eabdaffb1d67b07eda1a9a99f69fbe9e Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Fri, 27 Sep 2019 20:14:53 +0100 Subject: [PATCH 313/649] Further improvements --- arrow/factory.py | 2 +- arrow/util.py | 6 +++--- tests/factory_tests.py | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/arrow/factory.py b/arrow/factory.py index e112d5155..f9f2d6570 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -198,7 +198,7 @@ def get(self, *args, **kwargs): return self.type.utcfromtimestamp(calendar.timegm(arg)) # (iso calendar) -> convert then from date - elif isinstance(arg, tuple): + elif isinstance(arg, tuple) and len(arg) == 3: dt = iso_to_gregorian(*arg) return self.type.fromdate(dt) diff --git a/arrow/util.py b/arrow/util.py index ed6777016..7faea5ce8 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -20,13 +20,13 @@ def is_timestamp(value): # Credit to https://stackoverflow.com/a/1700069 def iso_to_gregorian(iso_year, iso_week, iso_day): - """Converts an iso weekday tuple into a datetime object.""" + """Converts an ISO week date tuple into a datetime object.""" if not 1 <= iso_week <= 53: - raise ValueError("Iso Calendar week value must be between 1-53.") + raise ValueError("ISO Calendar week value must be between 1-53.") if not 1 <= iso_day <= 7: - raise ValueError("Iso Calendar day value must be between 1-7") + raise ValueError("ISO Calendar day value must be between 1-7") # The first week of the year always contains 4 Jan. fourth_jan = datetime.date(iso_year, 1, 4) diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 8c00559ab..3b4c7a9db 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -150,6 +150,7 @@ def test_one_arg_iso_calendar(self): (datetime(2005, 1, 1), (2004, 53, 6)), (datetime(2010, 1, 4), (2010, 1, 1)), (datetime(2010, 1, 3), (2009, 53, 7)), + (datetime(2003, 12, 29), (2004, 1, 1)), ] for pair in pairs: From d693754c82f9ba116bda8e625d7fccaef513d514 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 1 Oct 2019 21:37:11 -0400 Subject: [PATCH 314/649] Added support for expanded timestamps --- arrow/constants.py | 7 +++++++ arrow/parser.py | 28 ++++++++++++++++++++++++-- tests/parser_tests.py | 47 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 arrow/constants.py diff --git a/arrow/constants.py b/arrow/constants.py new file mode 100644 index 000000000..6a916d32d --- /dev/null +++ b/arrow/constants.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +import time +from datetime import datetime + +MAX_TIMESTAMP = time.mktime(datetime.max.timetuple()) +MAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1000 +MAX_TIMESTAMP_US = MAX_TIMESTAMP * 1000000 diff --git a/arrow/parser.py b/arrow/parser.py index 2720d3c18..eb7888726 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -7,6 +7,7 @@ from dateutil import tz from arrow import locales +from arrow.constants import MAX_TIMESTAMP, MAX_TIMESTAMP_MS, MAX_TIMESTAMP_US try: from functools import lru_cache @@ -30,7 +31,7 @@ class ParserMatchError(ParserError): class DateTimeParser(object): _FORMAT_RE = 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)" + 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)" ) _ESCAPE_RE = re.compile(r"\[[^\[\]]*\]") @@ -45,7 +46,8 @@ class DateTimeParser(object): _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will # break cases like "15 Jul 2000" and a format list (see issue #447) - _TIMESTAMP_RE = re.compile(r"^-?\d+\.?\d+$") + _TIMESTAMP_RE = re.compile(r"^\-?\d+\.?\d+$") + _TIMESTAMP_EXPANDED_RE = re.compile(r"^\-?\d+$") _TIME_RE = re.compile(r"^(\d{2})(?:\:?(\d{2}))?(?:\:?(\d{2}))?(?:([\.\,])(\d+))?$") _BASE_INPUT_RE_MAP = { @@ -66,6 +68,7 @@ class DateTimeParser(object): "ss": _TWO_DIGIT_RE, "s": _ONE_OR_TWO_DIGIT_RE, "X": _TIMESTAMP_RE, + "x": _TIMESTAMP_EXPANDED_RE, "ZZZ": _TZ_NAME_RE, "ZZ": _TZ_ZZ_RE, "Z": _TZ_Z_RE, @@ -347,6 +350,9 @@ def _parse_token(self, token, value, parts): elif token == "X": parts["timestamp"] = float(value) + elif token == "x": + parts["expanded_timestamp"] = int(value) + elif token in ["ZZZ", "ZZ", "Z"]: parts["tzinfo"] = TzinfoParser.parse(value) @@ -364,6 +370,24 @@ def _build_datetime(parts): if timestamp is not None: return datetime.fromtimestamp(timestamp, tz=tz.tzutc()) + expanded_timestamp = parts.get("expanded_timestamp") + + if expanded_timestamp is not None: + + if expanded_timestamp > MAX_TIMESTAMP: + if expanded_timestamp < MAX_TIMESTAMP_MS: + expanded_timestamp /= 1000 + elif expanded_timestamp < MAX_TIMESTAMP_US: + expanded_timestamp /= 1000000 + else: + raise ValueError( + "The specified timestamp '{}' is too large.".format( + expanded_timestamp + ) + ) + + return datetime.fromtimestamp(expanded_timestamp, tz=tz.tzutc()) + day_of_year = parts.get("day_of_year") if day_of_year is not None: diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 48cff7dc8..8e8e025f2 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -10,6 +10,7 @@ from dateutil import tz from arrow import parser +from arrow.constants import MAX_TIMESTAMP_US from arrow.parser import DateTimeParser, ParserError, ParserMatchError @@ -257,6 +258,36 @@ def test_parse_timestamp(self): with self.assertRaises(ParserError): self.parser.parse(".1565982019", "X") + def test_parse_expanded_timestamp(self): + # test expanded timestamps that include milliseconds + # and nanoseconds as multiples rather than decimals + # requested in issue #357 + + tz_utc = tz.tzutc() + timestamp = time.time() + timestamp_milli = int(round(timestamp * 1000)) + timestamp_nano = int(round(timestamp * 1000000)) + + # "x" token should parse integer timestamps below MAX_TIMESTAMP normally + self.expected = datetime.fromtimestamp(int(timestamp), tz=tz_utc) + self.assertEqual( + self.parser.parse("{:d}".format(int(timestamp)), "x"), self.expected + ) + + self.expected = datetime.fromtimestamp(round(timestamp, 3), tz=tz_utc) + self.assertEqual( + self.parser.parse("{:d}".format(timestamp_milli), "x"), self.expected + ) + + self.expected = datetime.fromtimestamp(timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:d}".format(timestamp_nano), "x"), self.expected + ) + + # anything above max ns timestamp should fail + with self.assertRaises(ValueError): + self.parser.parse("{:d}".format(int(MAX_TIMESTAMP_US) + 1), "x") + def test_parse_names(self): self.expected = datetime(2012, 1, 1) @@ -619,6 +650,10 @@ def test_format_timestamp(self): self.assertEqual(self.format_regex.findall("X"), ["X"]) + def test_format_timestamp_milli(self): + + self.assertEqual(self.format_regex.findall("x"), ["x"]) + def test_escape(self): escape_regex = parser.DateTimeParser._ESCAPE_RE @@ -690,10 +725,22 @@ def test_timestamp(self): self.assertEqual( timestamp_re.findall("1565707550.452729"), ["1565707550.452729"] ) + self.assertEqual( + timestamp_re.findall("-1565707550.452729"), ["-1565707550.452729"] + ) + self.assertEqual(timestamp_re.findall("-1565707550"), ["-1565707550"]) self.assertEqual(timestamp_re.findall("1565707550"), ["1565707550"]) self.assertEqual(timestamp_re.findall("1565707550."), []) self.assertEqual(timestamp_re.findall(".1565707550"), []) + def test_timestamp_milli(self): + timestamp_milli_re = parser.DateTimeParser._TIMESTAMP_MILLI_RE + self.assertEqual(timestamp_milli_re.findall("-1565707550"), ["-1565707550"]) + self.assertEqual(timestamp_milli_re.findall("1565707550"), ["1565707550"]) + self.assertEqual(timestamp_milli_re.findall("1565707550.452729"), []) + self.assertEqual(timestamp_milli_re.findall("1565707550."), []) + self.assertEqual(timestamp_milli_re.findall(".1565707550"), []) + def test_time(self): time_re = parser.DateTimeParser._TIME_RE time_seperators = [":", ""] From bce8f60e43b97b07a2d9cac82dc98b5ead30cbb4 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 1 Oct 2019 21:57:05 -0400 Subject: [PATCH 315/649] Fixed a few bugs an added token to docs --- arrow/parser.py | 4 ++-- docs/index.rst | 4 +++- tests/parser_tests.py | 18 +++++++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index eb7888726..b68c03af4 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -376,9 +376,9 @@ def _build_datetime(parts): if expanded_timestamp > MAX_TIMESTAMP: if expanded_timestamp < MAX_TIMESTAMP_MS: - expanded_timestamp /= 1000 + expanded_timestamp /= 1000.0 elif expanded_timestamp < MAX_TIMESTAMP_US: - expanded_timestamp /= 1000000 + expanded_timestamp /= 1000000.0 else: raise ValueError( "The specified timestamp '{}' is too large.".format( diff --git a/docs/index.rst b/docs/index.rst index f6c9ff7e4..0e6d37e73 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -365,7 +365,9 @@ Use the following tokens in parsing and formatting. Note that they're not the s +--------------------------------+--------------+-------------------------------------------+ | |Z |-0700, -0600 ... +0600, +0700, +08, Z | +--------------------------------+--------------+-------------------------------------------+ -|**Timestamp** |X |1381685817, 1381685817.915482 ... [#t5]_ | +|**Seconds Timestamp** |X |1381685817, 1381685817.915482 ... [#t5]_ | ++--------------------------------+--------------+-------------------------------------------+ +|**ms or µs Timestamp** |x |1569980330813, 1569980330813221 | +--------------------------------+--------------+-------------------------------------------+ .. rubric:: Footnotes diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 8e8e025f2..cd0f67fc9 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -260,13 +260,13 @@ def test_parse_timestamp(self): def test_parse_expanded_timestamp(self): # test expanded timestamps that include milliseconds - # and nanoseconds as multiples rather than decimals + # and microseconds as multiples rather than decimals # requested in issue #357 tz_utc = tz.tzutc() timestamp = time.time() timestamp_milli = int(round(timestamp * 1000)) - timestamp_nano = int(round(timestamp * 1000000)) + timestamp_micro = int(round(timestamp * 1000000)) # "x" token should parse integer timestamps below MAX_TIMESTAMP normally self.expected = datetime.fromtimestamp(int(timestamp), tz=tz_utc) @@ -281,7 +281,7 @@ def test_parse_expanded_timestamp(self): self.expected = datetime.fromtimestamp(timestamp, tz=tz_utc) self.assertEqual( - self.parser.parse("{:d}".format(timestamp_nano), "x"), self.expected + self.parser.parse("{:d}".format(timestamp_micro), "x"), self.expected ) # anything above max ns timestamp should fail @@ -734,12 +734,12 @@ def test_timestamp(self): self.assertEqual(timestamp_re.findall(".1565707550"), []) def test_timestamp_milli(self): - timestamp_milli_re = parser.DateTimeParser._TIMESTAMP_MILLI_RE - self.assertEqual(timestamp_milli_re.findall("-1565707550"), ["-1565707550"]) - self.assertEqual(timestamp_milli_re.findall("1565707550"), ["1565707550"]) - self.assertEqual(timestamp_milli_re.findall("1565707550.452729"), []) - self.assertEqual(timestamp_milli_re.findall("1565707550."), []) - self.assertEqual(timestamp_milli_re.findall(".1565707550"), []) + timestamp_expanded_re = parser.DateTimeParser._TIMESTAMP_EXPANDED_RE + self.assertEqual(timestamp_expanded_re.findall("-1565707550"), ["-1565707550"]) + self.assertEqual(timestamp_expanded_re.findall("1565707550"), ["1565707550"]) + self.assertEqual(timestamp_expanded_re.findall("1565707550.452729"), []) + self.assertEqual(timestamp_expanded_re.findall("1565707550."), []) + self.assertEqual(timestamp_expanded_re.findall(".1565707550"), []) def test_time(self): time_re = parser.DateTimeParser._TIME_RE From 98ef9e3b4154fa52cff8b9652598627cf0b22936 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 1 Oct 2019 22:02:42 -0400 Subject: [PATCH 316/649] Updated comment --- tests/parser_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index cd0f67fc9..14b6f67ff 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -284,7 +284,7 @@ def test_parse_expanded_timestamp(self): self.parser.parse("{:d}".format(timestamp_micro), "x"), self.expected ) - # anything above max ns timestamp should fail + # anything above max µs timestamp should fail with self.assertRaises(ValueError): self.parser.parse("{:d}".format(int(MAX_TIMESTAMP_US) + 1), "x") From 67e3ec80a75f655229958e8bda592ec8448a2205 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 1 Oct 2019 22:24:22 -0400 Subject: [PATCH 317/649] Attempt fix for Windows and py35 --- arrow/constants.py | 6 +++--- tests/parser_tests.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/arrow/constants.py b/arrow/constants.py index 6a916d32d..0c33741c7 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -import time -from datetime import datetime -MAX_TIMESTAMP = time.mktime(datetime.max.timetuple()) +# output of time.mktime(datetime.max.timetuple()) on macOS +# must hardcode for compatibility with Windows +MAX_TIMESTAMP = 253402318799.0 MAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1000 MAX_TIMESTAMP_US = MAX_TIMESTAMP * 1000000 diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 14b6f67ff..8035fc6cd 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -264,7 +264,7 @@ def test_parse_expanded_timestamp(self): # requested in issue #357 tz_utc = tz.tzutc() - timestamp = time.time() + timestamp = 1569982581.413132 timestamp_milli = int(round(timestamp * 1000)) timestamp_micro = int(round(timestamp * 1000000)) From 2d977e1f25f1b0282b6dbdfd83b4e72efa225bfa Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 1 Oct 2019 22:31:28 -0400 Subject: [PATCH 318/649] Added comments to constants --- arrow/constants.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/arrow/constants.py b/arrow/constants.py index 0c33741c7..81e37b26d 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- -# output of time.mktime(datetime.max.timetuple()) on macOS -# must hardcode for compatibility with Windows +# Output of time.mktime(datetime.max.timetuple()) on macOS +# This value must be hardcoded for compatibility with Windows +# Platform-independent max timestamps are hard to form +# https://stackoverflow.com/q/46133223 MAX_TIMESTAMP = 253402318799.0 MAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1000 MAX_TIMESTAMP_US = MAX_TIMESTAMP * 1000000 From 9461692b4f238e0d695b750234cba9474935a6a9 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 3 Oct 2019 17:07:53 -0400 Subject: [PATCH 319/649] Refactored fromtimestamp and utcfromtimestamp methods in Arrow and improved performance of is_timestamp (#682) --- arrow/arrow.py | 25 +++++++++++++------------ arrow/factory.py | 12 ++++++------ arrow/util.py | 9 +++++++-- tests/arrow_tests.py | 23 +++++++++++++---------- tests/factory_tests.py | 4 ++-- tests/util_tests.py | 17 +++++++---------- 6 files changed, 48 insertions(+), 42 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 1b05b42bb..3790b35bb 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -150,8 +150,13 @@ def fromtimestamp(cls, timestamp, tzinfo=None): if tzinfo is None: tzinfo = dateutil_tz.tzlocal() - timestamp = cls._get_timestamp_from_input(timestamp) - dt = datetime.fromtimestamp(timestamp, tzinfo) + + if not util.is_timestamp(timestamp): + raise ValueError( + "The provided timestamp '{}' is invalid.".format(timestamp) + ) + + dt = datetime.fromtimestamp(float(timestamp), tzinfo) return cls( dt.year, @@ -172,8 +177,12 @@ def utcfromtimestamp(cls, timestamp): """ - timestamp = cls._get_timestamp_from_input(timestamp) - dt = datetime.utcfromtimestamp(timestamp) + if not util.is_timestamp(timestamp): + raise ValueError( + "The provided timestamp '{}' is invalid.".format(timestamp) + ) + + dt = datetime.utcfromtimestamp(float(timestamp)) return cls( dt.year, @@ -1382,14 +1391,6 @@ def _get_iteration_params(cls, end, limit): return end, sys.maxsize return end, limit - @staticmethod - def _get_timestamp_from_input(timestamp): - - try: - return float(timestamp) - except Exception: - raise ValueError("cannot parse '{}' as a timestamp".format(timestamp)) - Arrow.min = Arrow.fromdatetime(datetime.min) Arrow.max = Arrow.fromdatetime(datetime.max) diff --git a/arrow/factory.py b/arrow/factory.py index f9f2d6570..8bdccc9db 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -169,19 +169,19 @@ def get(self, *args, **kwargs): return self.type.utcnow() # try (int, float) -> utc, from timestamp. - if is_timestamp(arg): + elif not isstr(arg) and is_timestamp(arg): return self.type.utcfromtimestamp(arg) # (Arrow) -> from the object's datetime. - if isinstance(arg, Arrow): + elif isinstance(arg, Arrow): return self.type.fromdatetime(arg.datetime) # (datetime) -> from datetime. - if isinstance(arg, datetime): + elif isinstance(arg, datetime): return self.type.fromdatetime(arg) # (date) -> from date. - if isinstance(arg, date): + elif isinstance(arg, date): return self.type.fromdate(arg) # (tzinfo) -> now, @ tzinfo. @@ -204,7 +204,7 @@ def get(self, *args, **kwargs): else: raise TypeError( - "Can't parse single argument type of '{}'".format(type(arg)) + "Can't parse single argument of type '{}'".format(type(arg)) ) elif arg_count == 2: @@ -242,7 +242,7 @@ def get(self, *args, **kwargs): else: raise TypeError( - "Can't parse two arguments of types '{}', '{}'".format( + "Can't parse two arguments of types '{}' and '{}'".format( type(arg_1), type(arg_2) ) ) diff --git a/arrow/util.py b/arrow/util.py index 7faea5ce8..62f1a0537 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -9,12 +9,17 @@ def total_seconds(td): # pragma: no cover def is_timestamp(value): + """Check if value is a valid timestamp.""" if isinstance(value, bool): return False + if not ( + isinstance(value, int) or isinstance(value, float) or isinstance(value, str) + ): + return False try: - datetime.datetime.fromtimestamp(value) + float(value) return True - except TypeError: + except ValueError: return False diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index f2436550e..4b26e6195 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -103,6 +103,19 @@ def test_fromtimestamp(self): datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), ) + with self.assertRaises(ValueError): + arrow.Arrow.fromtimestamp("invalid timestamp") + + def test_utcfromtimestamp(self): + + timestamp = time.time() + + result = arrow.Arrow.utcfromtimestamp(timestamp) + assertDtEqual(result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc())) + + with self.assertRaises(ValueError): + arrow.Arrow.utcfromtimestamp("invalid timestamp") + def test_fromdatetime(self): dt = datetime(2013, 2, 3, 12, 30, 45, 1) @@ -1805,16 +1818,6 @@ def test_get_tzinfo(self): get_tzinfo("abc") self.assertFalse("{}" in str(raise_ctx.exception)) - def test_get_timestamp_from_input(self): - - self.assertEqual(arrow.Arrow._get_timestamp_from_input(123), 123) - self.assertEqual(arrow.Arrow._get_timestamp_from_input(123.4), 123.4) - self.assertEqual(arrow.Arrow._get_timestamp_from_input("123"), 123.0) - self.assertEqual(arrow.Arrow._get_timestamp_from_input("123.4"), 123.4) - - with self.assertRaises(ValueError): - arrow.Arrow._get_timestamp_from_input("abc") - def test_get_iteration_params(self): self.assertEqual( diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 3b4c7a9db..98d9eca88 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -68,8 +68,8 @@ def test_one_arg_timestamp(self): self.factory.get(str(float_timestamp)) # Regression test for issue #216 - timestamp = 99999999999999999999999999 - # Python 3 raises `OverflowError`, Python 2 raises `ValueError` + # Python 3 raises OverflowError, Python 2 raises ValueError + timestamp = 99999999999999999999999999.99999999999999999999999999 with self.assertRaises((OverflowError, ValueError)): self.factory.get(timestamp) diff --git a/tests/util_tests.py b/tests/util_tests.py index 21e98c7c1..bb84c44b7 100644 --- a/tests/util_tests.py +++ b/tests/util_tests.py @@ -13,22 +13,19 @@ def test_is_timestamp(self): self.assertTrue(util.is_timestamp(timestamp_int)) self.assertTrue(util.is_timestamp(timestamp_float)) + self.assertTrue(util.is_timestamp(str(timestamp_int))) + self.assertTrue(util.is_timestamp(str(timestamp_float))) - self.assertFalse(util.is_timestamp(str(timestamp_int))) - self.assertFalse(util.is_timestamp(str(timestamp_float))) self.assertFalse(util.is_timestamp(True)) self.assertFalse(util.is_timestamp(False)) - full_datetime = "2019-06-23T13:12:42" - self.assertFalse(util.is_timestamp(full_datetime)) + class InvalidTimestamp: + pass - overflow_timestamp_float = 99999999999999999999999999.99999999999999999999999999 - with self.assertRaises((OverflowError, ValueError)): - util.is_timestamp(overflow_timestamp_float) + self.assertFalse(util.is_timestamp(InvalidTimestamp())) - overflow_timestamp_int = int(overflow_timestamp_float) - with self.assertRaises((OverflowError, ValueError)): - util.is_timestamp(overflow_timestamp_int) + full_datetime = "2019-06-23T13:12:42" + self.assertFalse(util.is_timestamp(full_datetime)) def test_iso_gregorian(self): with self.assertRaises(ValueError): From 187a9a6c3c892e95d71aef8ebab8885bb141abb4 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 3 Oct 2019 17:22:04 -0400 Subject: [PATCH 320/649] Added Travis CI config for macOS (#683) * Added Travis CI config for macOS * Updated pre-commit hooks --- .pre-commit-config.yaml | 2 +- .travis.yml | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f8f133f43..99c8f7cb2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v1.23.0 + rev: v1.24.0 hooks: - id: pyupgrade - repo: https://github.com/pre-commit/pygrep-hooks diff --git a/.travis.yml b/.travis.yml index ba993ab8b..b86f51bca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,15 +15,20 @@ matrix: - name: "Python 3.7" python: "3.7" env: TOXENV=py37 + - name: "Python 3.7 on macOS" + os: osx + osx_image: xcode11 + language: shell # 'language: python' is an error on Travis CI macOS + env: TOXENV=py37 - name: "Python 3.7 on Windows" os: windows language: shell # 'language: python' is an error on Travis CI Windows before_install: - - choco install python --version 3.7.4 - - python -m pip install --upgrade pip + - choco install python --version 3.7.4 + - python -m pip install --upgrade pip env: - - PATH=/c/Python37:/c/Python37/Scripts:$PATH - - TOXENV=py37 + - PATH=/c/Python37:/c/Python37/Scripts:$PATH + - TOXENV=py37 - name: "Python 3.8" python: "3.8-dev" env: TOXENV=py38 From 17ed93be41d116ea3f9646ca69c48249ef2c2c60 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 7 Oct 2019 12:02:19 -0400 Subject: [PATCH 321/649] Added a test for floats in expanded timestamp --- tests/parser_tests.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 8035fc6cd..2ad133586 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -288,6 +288,10 @@ def test_parse_expanded_timestamp(self): with self.assertRaises(ValueError): self.parser.parse("{:d}".format(int(MAX_TIMESTAMP_US) + 1), "x") + # floats are not allowed with the "x" token + with self.assertRaises(ParserMatchError): + self.parser.parse("{:f}".format(timestamp), "x") + def test_parse_names(self): self.expected = datetime(2012, 1, 1) From 9e737346c3057dfb204105628701a07abf6edac1 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Wed, 9 Oct 2019 17:06:50 -0400 Subject: [PATCH 322/649] Renamed ISO 8601 mentions in docs to conform with ISO 8601 Wikipedia page (#689) --- CHANGELOG.rst | 6 +++--- README.rst | 4 ++-- arrow/arrow.py | 4 ++-- arrow/factory.py | 4 ++-- arrow/parser.py | 4 ++-- docs/index.rst | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2b62ab3a0..9265b151a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -239,7 +239,7 @@ The following will work in v0.15.0: - [NEW] struct_time addition. (mhworth) - [NEW] Version grep (eirnym) -- [NEW] Default to ISO-8601 format (emonty) +- [NEW] Default to ISO 8601 format (emonty) - [NEW] Raise TypeError on comparison (sniekamp) - [NEW] Adding Macedonian(mk) locale (krisfremen) - [FIX] Fix for ISO seconds and fractional seconds (sdispater) (andrewelkins) @@ -321,7 +321,7 @@ The following will work in v0.15.0: 0.4.0 ----- -- [NEW] Format-free ISO-8601 parsing in factory ``get`` method +- [NEW] Format-free ISO 8601 parsing in factory ``get`` method - [NEW] Support for 'week' / 'weeks' in ``span``, ``range``, ``span_range``, ``floor`` and ``ceil`` - [NEW] Support for 'weeks' in ``replace`` - [NEW] Norwegian locale (Martinp) @@ -332,7 +332,7 @@ The following will work in v0.15.0: - [FIX] Corrected plurals of Ukrainian and Russian nouns (Catchagain) - [CHANGE] Old 0.1 ``arrow`` module method removed - [CHANGE] Dropped timestamp support in ``range`` and ``span_range`` (never worked correctly) -- [CHANGE] Dropped parsing of single string as tz string in factory ``get`` method (replaced by ISO-8601) +- [CHANGE] Dropped parsing of single string as tz string in factory ``get`` method (replaced by ISO 8601) 0.3.5 ----- diff --git a/README.rst b/README.rst index 0a9969242..2d86b39f2 100644 --- a/README.rst +++ b/README.rst @@ -41,7 +41,7 @@ Python's standard library and some other low-level modules have near-complete da - Too many types: date, time, datetime, tzinfo, timedelta, relativedelta, etc. - Timezones and timestamp conversions are verbose and unpleasant - Timezone naivety is the norm -- Gaps in functionality: ISO-8601 parsing, timespans, humanization +- Gaps in functionality: ISO 8601 parsing, timespans, humanization Features -------- @@ -52,7 +52,7 @@ Features - Provides super-simple creation options for many common input scenarios - :code:`shift` method with support for relative offsets, including weeks - Formats and parses strings automatically -- Wide support for ISO-8601 +- Wide support for ISO 8601 - Timezone conversion - Timestamp available as a property - Generates time spans, ranges, floors and ceilings for time frames ranging from microsecond to year diff --git a/arrow/arrow.py b/arrow/arrow.py index 3790b35bb..d752e393a 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -40,7 +40,7 @@ class Arrow(object): - A ``tzinfo`` object. - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO-8601 style, as in '+07:00'. + - A ``str`` in ISO 8601 style, as in '+07:00'. - A ``str``, one of the following: 'local', 'utc', 'UTC'. Usage:: @@ -423,7 +423,7 @@ def interval(cls, frame, start, end, interval=1, tz=None): - A ``tzinfo`` object. - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO-8601 style, as in '+07:00'. + - A ``str`` in ISO 8601 style, as in '+07:00'. - A ``str``, one of the following: 'local', 'utc', 'UTC'. Usage: diff --git a/arrow/factory.py b/arrow/factory.py index 8bdccc9db..b6d78b64d 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -69,12 +69,12 @@ def get(self, *args, **kwargs): >>> arrow.get(1367992474) - **One** ISO-8601-formatted ``str``, to parse it:: + **One** ISO 8601-formatted ``str``, to parse it:: >>> arrow.get('2013-09-29T01:26:43.830580') - **One** ISO-8601-formatted ``str``, in basic format, to parse it:: + **One** ISO 8601-formatted ``str``, in basic format, to parse it:: >>> arrow.get('20160413T133656.456289') diff --git a/arrow/parser.py b/arrow/parser.py index b68c03af4..38696a071 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -110,7 +110,7 @@ def __init__(self, locale="en_us", cache_size=0): self._generate_pattern_re ) - # TODO: since we support more than ISO-8601, we should rename this function + # TODO: since we support more than ISO 8601, we should rename this function # IDEA: break into multiple functions def parse_iso(self, datetime_string): # TODO: add a flag to normalize whitespace (useful in logs, ref issue #421) @@ -128,7 +128,7 @@ def parse_iso(self, datetime_string): has_time = has_space_divider or has_t_divider has_tz = False - # date formats (ISO-8601 and others) to test against + # date formats (ISO 8601 and others) to test against # NOTE: YYYYMM is omitted to avoid confusion with YYMMDD (no longer part of ISO 8601, but is still often used) formats = [ "YYYY-MM-DD", diff --git a/docs/index.rst b/docs/index.rst index 0e6d37e73..ca07c34cd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -67,7 +67,7 @@ Search a date in a string: >>> arrow.get('June was born in May 1980', 'MMMM YYYY') -Some ISO-8601 compliant strings are recognized and parsed without a format string: +Some ISO 8601 compliant strings are recognized and parsed without a format string: >>> arrow.get('2013-09-30T15:34:00.000-07:00') From 373058a253f09d5b16da35e6fc3c04191ad54f1a Mon Sep 17 00:00:00 2001 From: Konstantin Kalentev Date: Sat, 19 Oct 2019 17:29:33 +0200 Subject: [PATCH 323/649] Test TzinfoParser against full timezone database Closes: crsmithdev/arrow#657 --- tests/parser_tests.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 2ad133586..798c04de4 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -6,14 +6,22 @@ import time from datetime import datetime +import pytz from chai import Chai from dateutil import tz +from dateutil.zoneinfo import get_zonefile_instance from arrow import parser from arrow.constants import MAX_TIMESTAMP_US from arrow.parser import DateTimeParser, ParserError, ParserMatchError +def make_full_tz_list(): + dateutil_zones = set(get_zonefile_instance().zones) + pytz_zones = set(pytz.all_timezones) + return dateutil_zones.union(pytz_zones) + + class DateTimeParserTests(Chai): def setUp(self): super(DateTimeParserTests, self).setUp() @@ -332,23 +340,7 @@ def test_parse_tz_zz(self): ) def test_parse_tz_name_zzz(self): - for tz_name in ( - # best solution would be to test on every available tz name from - # the tz database but it is actually tricky to retrieve them from - # dateutil so here is short list that should match all - # naming patterns/conventions in used tz database - "Africa/Tripoli", - "America/Port_of_Spain", - "Australia/LHI", - "Etc/GMT-11", - "Etc/GMT0", - "Etc/UCT", - "Etc/GMT+9", - "GMT+0", - "CST6CDT", - "GMT-0", - "W-SU", - ): + for tz_name in make_full_tz_list(): self.expected = datetime(2013, 1, 1, tzinfo=tz.gettz(tz_name)) self.assertEqual( self.parser.parse("2013-01-01 %s" % tz_name, "YYYY-MM-DD ZZZ"), From eb025fbe8c1e47b88a12c35fe82bf992e4fe5439 Mon Sep 17 00:00:00 2001 From: Vir <20403550+QuynhVir@users.noreply.github.com> Date: Mon, 21 Oct 2019 17:22:35 +0700 Subject: [PATCH 324/649] add "week" translation for vi-vn locale ```ValueError: Humanization of the 'weeks' granularity is not currently translated in the 'vi_vn' locale. Please consider making a contribution to this locale.``` --- arrow/locales.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 1cf17e440..1a50662e9 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1995,6 +1995,8 @@ class VietnameseLocale(Locale): "hours": "{0} giờ", "day": "một ngày", "days": "{0} ngày", + "week": "một tuần", + "weeks": "{0} tuần", "month": "một tháng", "months": "{0} tháng", "year": "một năm", From ea8ea9a4d067c9f02cb51fcae5ddfbfe89e02920 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Mon, 21 Oct 2019 17:51:51 +0100 Subject: [PATCH 325/649] Update tz formatter tests --- tests/formatter_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/formatter_tests.py b/tests/formatter_tests.py index f28a0e851..0ed94f340 100644 --- a/tests/formatter_tests.py +++ b/tests/formatter_tests.py @@ -122,7 +122,7 @@ def test_timezone(self): def test_timezone_formatter(self): tz_map = { - # 'BRST': 'America/Sao_Paulo', TODO investigate why this fails + "CST": "Asia/Shanghai", "CET": "Europe/Berlin", "JST": "Asia/Tokyo", "PST": "US/Pacific", From 6950788d558eaf568502aef8c958a7cc53c53cc8 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Mon, 21 Oct 2019 18:48:42 +0100 Subject: [PATCH 326/649] Move todo note --- arrow/formatter.py | 2 +- arrow/parser.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/arrow/formatter.py b/arrow/formatter.py index 08c89e04d..ad1ac671e 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -10,7 +10,7 @@ class DateTimeFormatter(object): - + # TODO: test against full timezone DB _FORMAT_RE = re.compile( r"(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X)" ) diff --git a/arrow/parser.py b/arrow/parser.py index 38696a071..be7e0be77 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -474,7 +474,6 @@ def _generate_choice_re(choices, flags=0): class TzinfoParser(object): - # TODO: test against full timezone DB _TZINFO_RE = re.compile(r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$") @classmethod From e2c8eb5a4de1132f4eae16d99670b2cf5dc264f4 Mon Sep 17 00:00:00 2001 From: kkoziara Date: Tue, 22 Oct 2019 21:42:44 +0200 Subject: [PATCH 327/649] Added bounds parameter to Arrow.span_range, Arrow.interval and Arrow.span (#687). Parameter allows to specify whether returned intervals should be closed, open, left-open or right-open. --- arrow/arrow.py | 51 +++++++++++++++++++++++++---------- tests/arrow_tests.py | 64 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 14 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index d752e393a..5bfe24012 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -351,7 +351,7 @@ def range(cls, frame, start, end=None, tz=None, limit=None): ) @classmethod - def span_range(cls, frame, start, end, tz=None, limit=None): + def span_range(cls, frame, start, end, tz=None, limit=None, bounds="[)"): """ Returns an iterator of tuples, each :class:`Arrow ` objects, representing a series of timespans between two inputs. @@ -361,6 +361,10 @@ def span_range(cls, frame, start, end, tz=None, limit=None): :param tz: (optional) A :ref:`timezone expression `. Defaults to ``start``'s timezone, or UTC if ``start`` is naive. :param limit: (optional) A maximum number of tuples to return. + :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies + whether to include or exclude the start and end values in the span. '(' excludes + the start, '[' includes the start, ')' excludes the end, and ']' includes the end. + If the bounds are not specified, the default bound '[)' is used. **NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to return the entire range. Call with ``limit`` alone to return a maximum # of results from @@ -399,10 +403,10 @@ def span_range(cls, frame, start, end, tz=None, limit=None): tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) start = cls.fromdatetime(start, tzinfo).span(frame)[0] _range = cls.range(frame, start, end, tz, limit) - return (r.span(frame) for r in _range) + return (r.span(frame, bounds=bounds) for r in _range) @classmethod - def interval(cls, frame, start, end, interval=1, tz=None): + def interval(cls, frame, start, end, interval=1, tz=None, bounds="[)"): """ Returns an iterator of tuples, each :class:`Arrow ` objects, representing a series of intervals between two inputs. @@ -411,6 +415,10 @@ def interval(cls, frame, start, end, interval=1, tz=None): :param end: (optional) A datetime expression, the end of the range. :param interval: (optional) Time interval for the given time frame. :param tz: (optional) A timezone expression. Defaults to UTC. + :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies + whether to include or exclude the start and end values in the span. '(' excludes + the start, '[' includes the start, ')' excludes the end, and ']' includes the end. + If the bounds are not specified, the default bound '[)' is used. Supported frame values: year, quarter, month, week, day, hour, minute, second @@ -440,7 +448,7 @@ def interval(cls, frame, start, end, interval=1, tz=None): if interval < 1: raise ValueError("interval has to be a positive integer") - spanRange = iter(cls.span_range(frame, start, end, tz)) + spanRange = iter(cls.span_range(frame, start, end, tz, bounds=bounds)) while True: try: intvlStart, intvlEnd = next(spanRange) @@ -720,12 +728,23 @@ def to(self, tz): dt.tzinfo, ) - def span(self, frame, count=1): + @classmethod + def _validate_bounds(cls, bounds): + if bounds != "()" and bounds != "(]" and bounds != "[)" and bounds != "[]": + raise AttributeError( + 'Invalid bounds. Please select between "()", "(]", "[)", or "[]".' + ) + + def span(self, frame, count=1, bounds="[)"): """ Returns two new :class:`Arrow ` objects, representing the timespan of the :class:`Arrow ` object in a given timeframe. :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). :param count: (optional) the number of frames to span. + :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies + whether to include or exclude the start and end values in the span. '(' excludes + the start, '[' includes the start, ')' excludes the end, and ']' includes the end. + If the bounds are not specified, the default bound '[)' is used. Supported frame values: year, quarter, month, week, day, hour, minute, second. @@ -743,8 +762,13 @@ def span(self, frame, count=1): >>> arrow.utcnow().span('day', count=2) (, ) + >>> arrow.utcnow().span('day', bounds='[]') + (, ) + """ + self._validate_bounds(bounds) + frame_absolute, frame_relative, relative_steps = self._get_frames(frame) if frame_absolute == "week": @@ -769,11 +793,13 @@ def span(self, frame, count=1): elif frame_absolute == "quarter": floor = floor + relativedelta(months=-((self.month - 1) % 3)) - ceil = ( - floor - + relativedelta(**{frame_relative: count * relative_steps}) - + relativedelta(microseconds=-1) - ) + ceil = floor + relativedelta(**{frame_relative: count * relative_steps}) + + if bounds[0] == "(": + floor += relativedelta(microseconds=1) + + if bounds[1] == ")": + ceil += relativedelta(microseconds=-1) return floor, ceil @@ -1001,10 +1027,7 @@ def is_between(self, start, end, bounds="()"): """ - if bounds != "()" and bounds != "(]" and bounds != "[)" and bounds != "[]": - raise AttributeError( - 'Invalid bounds. Please select between "()", "(]", "[)", or "[]".' - ) + self._validate_bounds(bounds) if not isinstance(start, Arrow): raise TypeError( diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 4b26e6195..1beec2600 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1207,6 +1207,22 @@ def test_aware_tz(self): self.assertEqual(f.tzinfo, tz.gettz("US/Central")) self.assertEqual(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="[]" + ) + ) + + self.assertEqual( + result, + [ + (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 4, 1)), + (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 7, 1)), + ], + ) + class ArrowIntervalTests(Chai): def test_incorrect_input(self): @@ -1247,6 +1263,26 @@ def test_correct(self): ], ) + def test_bounds_param_is_passed(self): + result = list( + arrow.Arrow.interval( + "hour", + datetime(2013, 5, 5, 12, 30), + datetime(2013, 5, 5, 17, 15), + 2, + bounds="[]", + ) + ) + + self.assertEqual( + result, + [ + (arrow.Arrow(2013, 5, 5, 12), arrow.Arrow(2013, 5, 5, 14)), + (arrow.Arrow(2013, 5, 5, 14), arrow.Arrow(2013, 5, 5, 16)), + (arrow.Arrow(2013, 5, 5, 16), arrow.Arrow(2013, 5, 5, 18)), + ], + ) + class ArrowSpanTests(Chai): def setUp(self): @@ -1368,6 +1404,34 @@ def test_floor(self): self.assertEqual(floor, self.arrow.floor("month")) self.assertEqual(ceil, self.arrow.ceil("month")) + def test_span_inclusive_inclusive(self): + + floor, ceil = self.arrow.span("hour", bounds="[]") + + self.assertEqual(floor, datetime(2013, 2, 15, 3, tzinfo=tz.tzutc())) + self.assertEqual(ceil, datetime(2013, 2, 15, 4, tzinfo=tz.tzutc())) + + def test_span_exclusive_inclusive(self): + + floor, ceil = self.arrow.span("hour", bounds="(]") + + self.assertEqual(floor, datetime(2013, 2, 15, 3, 0, 0, 1, tzinfo=tz.tzutc())) + self.assertEqual(ceil, datetime(2013, 2, 15, 4, tzinfo=tz.tzutc())) + + def test_span_exclusive_exclusive(self): + + floor, ceil = self.arrow.span("hour", bounds="()") + + self.assertEqual(floor, datetime(2013, 2, 15, 3, 0, 0, 1, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 2, 15, 3, 59, 59, 999999, tzinfo=tz.tzutc()) + ) + + def test_bounds_are_validated(self): + + with self.assertRaises(AttributeError): + floor, ceil = self.arrow.span("hour", bounds="][") + class ArrowHumanizeTests(Chai): def setUp(self): From 40b7478b9e27a12e2f5652c45d37fae62912699b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernanda=20Guimar=C3=A3es?= Date: Fri, 25 Oct 2019 15:27:26 -0300 Subject: [PATCH 328/649] Test formatter against full timezone database #693 --- tests/formatter_tests.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/formatter_tests.py b/tests/formatter_tests.py index 0ed94f340..8ec336661 100644 --- a/tests/formatter_tests.py +++ b/tests/formatter_tests.py @@ -5,10 +5,17 @@ import pytz from chai import Chai from dateutil import tz as dateutil_tz +from dateutil.zoneinfo import get_zonefile_instance from arrow import formatter +def make_full_tz_list(): + dateutil_zones = set(get_zonefile_instance().zones) + pytz_zones = set(pytz.all_timezones) + return dateutil_zones.union(pytz_zones) + + class DateTimeFormatterFormatTokenTests(Chai): def setUp(self): super(DateTimeFormatterFormatTokenTests, self).setUp() @@ -121,18 +128,13 @@ def test_timezone(self): def test_timezone_formatter(self): - tz_map = { - "CST": "Asia/Shanghai", - "CET": "Europe/Berlin", - "JST": "Asia/Tokyo", - "PST": "US/Pacific", - } - - for abbreviation, full_name in tz_map.items(): + for full_name in make_full_tz_list(): # 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_name) ) + abbreviation = dt.tzname() + result = self.formatter._format_token(dt, "ZZZ") self.assertEqual(result, abbreviation) From 00ee9c2e9ccfba3b9f13adcefff71f17573d5702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernanda=20Guimar=C3=A3es?= Date: Sat, 26 Oct 2019 13:49:22 -0300 Subject: [PATCH 329/649] Extracted make_full_tz_list to utils. --- tests/formatter_tests.py | 8 +------- tests/parser_tests.py | 9 +-------- tests/utils.py | 9 +++++++++ 3 files changed, 11 insertions(+), 15 deletions(-) create mode 100644 tests/utils.py diff --git a/tests/formatter_tests.py b/tests/formatter_tests.py index 8ec336661..805b8ec72 100644 --- a/tests/formatter_tests.py +++ b/tests/formatter_tests.py @@ -5,15 +5,9 @@ import pytz from chai import Chai from dateutil import tz as dateutil_tz -from dateutil.zoneinfo import get_zonefile_instance from arrow import formatter - - -def make_full_tz_list(): - dateutil_zones = set(get_zonefile_instance().zones) - pytz_zones = set(pytz.all_timezones) - return dateutil_zones.union(pytz_zones) +from tests.utils import make_full_tz_list class DateTimeFormatterFormatTokenTests(Chai): diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 798c04de4..68ad377c4 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -6,20 +6,13 @@ import time from datetime import datetime -import pytz from chai import Chai from dateutil import tz -from dateutil.zoneinfo import get_zonefile_instance from arrow import parser from arrow.constants import MAX_TIMESTAMP_US from arrow.parser import DateTimeParser, ParserError, ParserMatchError - - -def make_full_tz_list(): - dateutil_zones = set(get_zonefile_instance().zones) - pytz_zones = set(pytz.all_timezones) - return dateutil_zones.union(pytz_zones) +from tests.utils import make_full_tz_list class DateTimeParserTests(Chai): diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 000000000..bb469b165 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +import pytz +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) From 4d64d58f27f05143e1eb32935fc5bbf294af9038 Mon Sep 17 00:00:00 2001 From: kkoziara Date: Sun, 27 Oct 2019 21:10:25 +0100 Subject: [PATCH 330/649] Add support for negative timestamps on Windows (#675) --- arrow/arrow.py | 4 +-- arrow/parser.py | 6 ++--- arrow/util.py | 51 +++++++++++++++++++++++++++++++++++- tests/util_tests.py | 64 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 6 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index d752e393a..3d458b364 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -156,7 +156,7 @@ def fromtimestamp(cls, timestamp, tzinfo=None): "The provided timestamp '{}' is invalid.".format(timestamp) ) - dt = datetime.fromtimestamp(float(timestamp), tzinfo) + dt = util.safe_fromtimestamp(float(timestamp), tzinfo) return cls( dt.year, @@ -182,7 +182,7 @@ def utcfromtimestamp(cls, timestamp): "The provided timestamp '{}' is invalid.".format(timestamp) ) - dt = datetime.utcfromtimestamp(float(timestamp)) + dt = util.safe_utcfromtimestamp(float(timestamp)) return cls( dt.year, diff --git a/arrow/parser.py b/arrow/parser.py index be7e0be77..d3030ee98 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -6,7 +6,7 @@ from dateutil import tz -from arrow import locales +from arrow import locales, util from arrow.constants import MAX_TIMESTAMP, MAX_TIMESTAMP_MS, MAX_TIMESTAMP_US try: @@ -368,7 +368,7 @@ def _build_datetime(parts): timestamp = parts.get("timestamp") if timestamp is not None: - return datetime.fromtimestamp(timestamp, tz=tz.tzutc()) + return util.safe_fromtimestamp(timestamp, tz=tz.tzutc()) expanded_timestamp = parts.get("expanded_timestamp") @@ -386,7 +386,7 @@ def _build_datetime(parts): ) ) - return datetime.fromtimestamp(expanded_timestamp, tz=tz.tzutc()) + return util.safe_fromtimestamp(expanded_timestamp, tz=tz.tzutc()) day_of_year = parts.get("day_of_year") diff --git a/arrow/util.py b/arrow/util.py index 62f1a0537..287b1dcd0 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -2,6 +2,11 @@ from __future__ import absolute_import import datetime +import math +import os +import time + +import dateutil def total_seconds(td): # pragma: no cover @@ -23,6 +28,42 @@ def is_timestamp(value): return False +def datetime_from_timestamp(timestamp, tz=None): + """Computes datetime from timestamp. Supports negative timestamps on Windows platform.""" + sec_frac, sec = math.modf(timestamp) + dt = datetime.datetime(1970, 1, 1, tzinfo=dateutil.tz.tzutc()) + datetime.timedelta( + seconds=sec, microseconds=sec_frac * 1e6 + ) + if tz is None: + tz = dateutil.tz.tzlocal() + if tz == dateutil.tz.tzlocal(): + # because datetime.astimezone does not work on Windows for tzlocal() and dates before the 1970-01-01 + # take timestamp from appropriate time of the year, because of daylight saving time changes + ts = time.mktime(dt.replace(year=1970).timetuple()) + dt += datetime.datetime.fromtimestamp(ts) - datetime.datetime.utcfromtimestamp( + ts + ) + return dt.replace(tzinfo=dateutil.tz.tzlocal()) + else: + return dt.astimezone(tz) + + +def safe_utcfromtimestamp(timestamp, os_name=os.name): + """ datetime.utcfromtimestamp alternative which supports negatvie timestamps on Windows platform.""" + if os_name == "nt" and timestamp < 0: + return datetime_from_timestamp(timestamp, dateutil.tz.tzutc()) + else: + return datetime.datetime.utcfromtimestamp(timestamp) + + +def safe_fromtimestamp(timestamp, tz=None, os_name=os.name): + """ datetime.fromtimestamp alternative which supports negatvie timestamps on Windows platform.""" + if os_name == "nt" and timestamp < 0: + return datetime_from_timestamp(timestamp, tz) + else: + return datetime.datetime.fromtimestamp(timestamp, tz) + + # Credit to https://stackoverflow.com/a/1700069 def iso_to_gregorian(iso_year, iso_week, iso_day): """Converts an ISO week date tuple into a datetime object.""" @@ -57,4 +98,12 @@ def isstr(s): return isinstance(s, str) -__all__ = ["total_seconds", "is_timestamp", "isstr", "iso_to_gregorian"] +__all__ = [ + "total_seconds", + "is_timestamp", + "isstr", + "iso_to_gregorian", + "datetime_from_timestamp", + "safe_utcfromtimestamp", + "safe_fromtimestamp", +] diff --git a/tests/util_tests.py b/tests/util_tests.py index bb84c44b7..f88415f7b 100644 --- a/tests/util_tests.py +++ b/tests/util_tests.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- import time +from datetime import datetime from chai import Chai +from dateutil import tz from arrow import util @@ -33,3 +35,65 @@ def test_iso_gregorian(self): with self.assertRaises(ValueError): util.iso_to_gregorian(2013, 8, 0) + + def test_datetime_from_timestamp(self): + timestamp = 1572204340.6460679 + result = util.datetime_from_timestamp(timestamp) + expected = datetime.fromtimestamp(timestamp).replace(tzinfo=tz.tzlocal()) + self.assertEqual(result, expected) + + def test_datetime_from_timestamp_utc(self): + timestamp = 1572204340.6460679 + result = util.datetime_from_timestamp(timestamp, tz.tzutc()) + expected = datetime.utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) + self.assertEqual(result, expected) + + def test_safe_utcfromtimestamp(self): + timestamp = 1572204340.6460679 + result = util.safe_utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) + expected = datetime.utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) + self.assertEqual(result, expected) + + def test_safe_fromtimestamp_default_tz(self): + timestamp = 1572204340.6460679 + result = util.safe_fromtimestamp(timestamp).replace(tzinfo=tz.tzlocal()) + expected = datetime.fromtimestamp(timestamp).replace(tzinfo=tz.tzlocal()) + self.assertEqual(result, expected) + + def test_safe_fromtimestamp_paris_tz(self): + timestamp = 1572204340.6460679 + result = util.safe_fromtimestamp(timestamp, tz.gettz("Europe/Paris")) + expected = datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")) + self.assertEqual(result, expected) + + def test_safe_utcfromtimestamp_negative(self): + timestamp = -1572204340.6460679 + result = util.safe_utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) + expected = datetime(1920, 3, 7, 4, 34, 19, 353932, tzinfo=tz.tzutc()) + self.assertEqual(result, expected) + + def test_safe_fromtimestamp_negative(self): + timestamp = -1572204340.6460679 + result = util.safe_fromtimestamp(timestamp, tz.gettz("Europe/Paris")) + expected = datetime( + 1920, 3, 7, 5, 34, 19, 353932, tzinfo=tz.gettz("Europe/Paris") + ) + self.assertEqual(result, expected) + + def test_safe_utcfromtimestamp_negative_nt(self): + timestamp = -1572204340.6460679 + result = util.safe_utcfromtimestamp(timestamp, os_name="nt").replace( + tzinfo=tz.tzutc() + ) + expected = datetime(1920, 3, 7, 4, 34, 19, 353932, tzinfo=tz.tzutc()) + self.assertEqual(result, expected) + + def test_safe_fromtimestamp_negative_nt(self): + timestamp = -1572204340.6460679 + result = util.safe_fromtimestamp( + timestamp, tz.gettz("Europe/Paris"), os_name="nt" + ) + expected = datetime( + 1920, 3, 7, 5, 34, 19, 353932, tzinfo=tz.gettz("Europe/Paris") + ) + self.assertEqual(result, expected) From a4ee18f63d9943ff976ce45ddc6c9ed579068039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernanda=20Guimar=C3=A3es?= Date: Wed, 30 Oct 2019 10:48:22 -0300 Subject: [PATCH 331/649] Changed utils import style. --- tests/formatter_tests.py | 3 ++- tests/parser_tests.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/formatter_tests.py b/tests/formatter_tests.py index 805b8ec72..191be19a4 100644 --- a/tests/formatter_tests.py +++ b/tests/formatter_tests.py @@ -7,7 +7,8 @@ from dateutil import tz as dateutil_tz from arrow import formatter -from tests.utils import make_full_tz_list + +from .utils import make_full_tz_list class DateTimeFormatterFormatTokenTests(Chai): diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 68ad377c4..9677c88b0 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -12,7 +12,8 @@ from arrow import parser from arrow.constants import MAX_TIMESTAMP_US from arrow.parser import DateTimeParser, ParserError, ParserMatchError -from tests.utils import make_full_tz_list + +from .utils import make_full_tz_list class DateTimeParserTests(Chai): From 4bb36e89921adf849708ed023e51b87243174f2b Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 31 Oct 2019 11:20:39 +0000 Subject: [PATCH 332/649] Upgraded dependencies and re-ran black (#702) --- .pre-commit-config.yaml | 12 ++++++------ arrow/parser.py | 10 +++++++--- requirements.txt | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 99c8f7cb2..4abab574e 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: v2.3.0 + rev: v2.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -16,7 +16,7 @@ repos: - id: check-merge-conflict - id: debug-statements - repo: https://github.com/asottile/seed-isort-config - rev: v1.9.2 + rev: v1.9.3 hooks: - id: seed-isort-config - repo: https://github.com/pre-commit/mirrors-isort @@ -24,22 +24,22 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v1.24.0 + rev: v1.25.1 hooks: - id: pyupgrade - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.4.1 + rev: v1.4.2 hooks: - id: python-no-eval - id: python-check-blanket-noqa - id: rst-backticks - repo: https://github.com/psf/black - rev: 19.3b0 + rev: 19.10b0 hooks: - id: black args: [--safe, --quiet] - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.8 + rev: 3.7.9 hooks: - id: flake8 additional_dependencies: [flake8-bugbear] diff --git a/arrow/parser.py b/arrow/parser.py index be7e0be77..7efc419a8 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -165,9 +165,13 @@ def parse_iso(self, datetime_string): "Invalid time component provided. Please specify a format or provide a valid time component in the basic or extended ISO 8601 time format." ) - hours, minutes, seconds, subseconds_sep, subseconds = ( - time_components.groups() - ) + ( + hours, + minutes, + seconds, + subseconds_sep, + subseconds, + ) = time_components.groups() has_tz = len(time_parts) == 2 has_minutes = minutes is not None diff --git a/requirements.txt b/requirements.txt index 780f15d9b..46d326fd9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ dateparser==0.7.* mock==3.0.* nose==1.3.7 nose-cov==1.6 -pre-commit==1.18.* +pre-commit==1.20.* python-dateutil==2.8.* pytz==2019.* simplejson==3.16.* From a0ba91395dc02f92e58395e41db77af12439a507 Mon Sep 17 00:00:00 2001 From: neuged Date: Thu, 31 Oct 2019 15:05:07 +0100 Subject: [PATCH 333/649] Add ParserError to the module exports. Adding ParserError to the module exports allows users to easily catch that specific error. It also eases lookup of that error for tools like PyCharm. --- arrow/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/arrow/__init__.py b/arrow/__init__.py index 9b5fa6873..d99a70ab6 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -3,3 +3,4 @@ from .api import get, now, utcnow from .arrow import Arrow from .factory import ArrowFactory +from .parser import ParserError From 24e7a068f5ce3572c7d17a3c4b5d048efef51a4e Mon Sep 17 00:00:00 2001 From: Cesar Orlando Charria Mondragon Date: Wed, 30 Oct 2019 23:47:54 -0500 Subject: [PATCH 334/649] Add support for midnight at the end of day Fix #703 --- arrow/parser.py | 17 ++++++++- tests/parser_tests.py | 80 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/arrow/parser.py b/arrow/parser.py index 7efc419a8..a13b7152e 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -427,6 +427,21 @@ def _build_datetime(parts): elif am_pm == "am" and hour == 12: hour = 0 + # Support for midnight at the end of day + if hour == 24: + if parts.get("minute", 0) != 0: + raise ParserError("Midnight at the end of day must not contain minutes") + if parts.get("second", 0) != 0: + raise ParserError("Midnight at the end of day must not contain seconds") + if parts.get("microsecond", 0) != 0: + raise ParserError( + "Midnight at the end of day must not contain microseconds" + ) + hour = 0 + day_increment = 1 + else: + day_increment = 0 + # account for rounding up to 1000000 microsecond = parts.get("microsecond", 0) if microsecond == 1000000: @@ -435,7 +450,7 @@ def _build_datetime(parts): else: second_increment = 0 - increment = timedelta(seconds=second_increment) + increment = timedelta(days=day_increment, seconds=second_increment) return ( datetime( diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 9677c88b0..93eefc5c5 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -586,6 +586,48 @@ def test_parse_DDDD_only(self): with self.assertRaises(ParserError): self.parser.parse("145", "DDDD") + def test_parse_HH_24(self): + self.assertEqual( + self.parser.parse("2019-10-30T24:00:00", "YYYY-MM-DDTHH:mm:ss"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-10-30T24:00", "YYYY-MM-DDTHH:mm"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-10-30T24", "YYYY-MM-DDTHH"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-10-30T24:00:00.0", "YYYY-MM-DDTHH:mm:ss.S"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-10-31T24:00:00", "YYYY-MM-DDTHH:mm:ss"), + datetime(2019, 11, 1, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-12-31T24:00:00", "YYYY-MM-DDTHH:mm:ss"), + datetime(2020, 1, 1, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-12-31T23:59:59.9999999", "YYYY-MM-DDTHH:mm:ss.S"), + datetime(2020, 1, 1, 0, 0, 0, 0), + ) + + with self.assertRaises(ParserError): + self.parser.parse("2019-12-31T24:01:00", "YYYY-MM-DDTHH:mm:ss") + + with self.assertRaises(ParserError): + self.parser.parse("2019-12-31T24:00:01", "YYYY-MM-DDTHH:mm:ss") + + with self.assertRaises(ParserError): + self.parser.parse("2019-12-31T24:00:00.1", "YYYY-MM-DDTHH:mm:ss.S") + + with self.assertRaises(ParserError): + self.parser.parse("2019-12-31T24:00:00.999999", "YYYY-MM-DDTHH:mm:ss.S") + class DateTimeParserRegexTests(Chai): def setUp(self): @@ -1176,6 +1218,44 @@ def test_iso8601_basic_format(self): with self.assertRaises(ParserError): self.parser.parse_iso("20180517T1055213Z") + def test_midnight_end_day(self): + self.assertEqual( + self.parser.parse_iso("2019-10-30T24:00:00"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse_iso("2019-10-30T24:00"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse_iso("2019-10-30T24:00:00.0"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse_iso("2019-10-31T24:00:00"), + datetime(2019, 11, 1, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse_iso("2019-12-31T24:00:00"), + datetime(2020, 1, 1, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse_iso("2019-12-31T23:59:59.9999999"), + datetime(2020, 1, 1, 0, 0, 0, 0), + ) + + with self.assertRaises(ParserError): + self.parser.parse_iso("2019-12-31T24:01:00") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2019-12-31T24:00:01") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2019-12-31T24:00:00.1") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2019-12-31T24:00:00.999999") + class TzinfoParserTests(Chai): def setUp(self): From 8f6b82e444e0369bfd119c2699687a93a2192f12 Mon Sep 17 00:00:00 2001 From: kkoziara Date: Fri, 1 Nov 2019 17:54:03 +0100 Subject: [PATCH 335/649] Changed datetime_from_timestamp to windows_datetime_from_timestamp. Changed default timzeon for windows_datetime_from_timestamp to utc time instead of local time. --- arrow/util.py | 35 +++++++++++++++++++---------------- tests/util_tests.py | 14 ++++++++++---- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/arrow/util.py b/arrow/util.py index 287b1dcd0..d9e800e12 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -28,30 +28,30 @@ def is_timestamp(value): return False -def datetime_from_timestamp(timestamp, tz=None): +def windows_datetime_from_timestamp(timestamp, tz=None): """Computes datetime from timestamp. Supports negative timestamps on Windows platform.""" sec_frac, sec = math.modf(timestamp) dt = datetime.datetime(1970, 1, 1, tzinfo=dateutil.tz.tzutc()) + datetime.timedelta( seconds=sec, microseconds=sec_frac * 1e6 ) - if tz is None: - tz = dateutil.tz.tzlocal() - if tz == dateutil.tz.tzlocal(): - # because datetime.astimezone does not work on Windows for tzlocal() and dates before the 1970-01-01 - # take timestamp from appropriate time of the year, because of daylight saving time changes - ts = time.mktime(dt.replace(year=1970).timetuple()) - dt += datetime.datetime.fromtimestamp(ts) - datetime.datetime.utcfromtimestamp( - ts - ) - return dt.replace(tzinfo=dateutil.tz.tzlocal()) - else: - return dt.astimezone(tz) + if tz is not None: + if tz == dateutil.tz.tzlocal(): + # because datetime.astimezone does not work on Windows for tzlocal() and dates before the 1970-01-01 + # take timestamp from appropriate time of the year, because of daylight saving time changes + ts = time.mktime(dt.replace(year=1970).timetuple()) + dt += datetime.datetime.fromtimestamp( + ts + ) - datetime.datetime.utcfromtimestamp(ts) + dt = dt.replace(tzinfo=dateutil.tz.tzlocal()) + else: + dt = dt.astimezone(tz) + return dt def safe_utcfromtimestamp(timestamp, os_name=os.name): """ datetime.utcfromtimestamp alternative which supports negatvie timestamps on Windows platform.""" if os_name == "nt" and timestamp < 0: - return datetime_from_timestamp(timestamp, dateutil.tz.tzutc()) + return windows_datetime_from_timestamp(timestamp, dateutil.tz.tzutc()) else: return datetime.datetime.utcfromtimestamp(timestamp) @@ -59,7 +59,10 @@ def safe_utcfromtimestamp(timestamp, os_name=os.name): def safe_fromtimestamp(timestamp, tz=None, os_name=os.name): """ datetime.fromtimestamp alternative which supports negatvie timestamps on Windows platform.""" if os_name == "nt" and timestamp < 0: - return datetime_from_timestamp(timestamp, tz) + if tz is None: + # because datetime.fromtimestamp default is local time + tz = dateutil.tz.tzlocal() + return windows_datetime_from_timestamp(timestamp, tz) else: return datetime.datetime.fromtimestamp(timestamp, tz) @@ -103,7 +106,7 @@ def isstr(s): "is_timestamp", "isstr", "iso_to_gregorian", - "datetime_from_timestamp", + "windows_datetime_from_timestamp", "safe_utcfromtimestamp", "safe_fromtimestamp", ] diff --git a/tests/util_tests.py b/tests/util_tests.py index f88415f7b..f2bc9d5a6 100644 --- a/tests/util_tests.py +++ b/tests/util_tests.py @@ -36,15 +36,15 @@ def test_iso_gregorian(self): with self.assertRaises(ValueError): util.iso_to_gregorian(2013, 8, 0) - def test_datetime_from_timestamp(self): + def test_windows_datetime_from_timestamp(self): timestamp = 1572204340.6460679 - result = util.datetime_from_timestamp(timestamp) + result = util.windows_datetime_from_timestamp(timestamp) expected = datetime.fromtimestamp(timestamp).replace(tzinfo=tz.tzlocal()) self.assertEqual(result, expected) - def test_datetime_from_timestamp_utc(self): + def test_windows_datetime_from_timestamp_utc(self): timestamp = 1572204340.6460679 - result = util.datetime_from_timestamp(timestamp, tz.tzutc()) + result = util.windows_datetime_from_timestamp(timestamp, tz.tzutc()) expected = datetime.utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) self.assertEqual(result, expected) @@ -97,3 +97,9 @@ def test_safe_fromtimestamp_negative_nt(self): 1920, 3, 7, 5, 34, 19, 353932, tzinfo=tz.gettz("Europe/Paris") ) self.assertEqual(result, expected) + + def test_safe_fromtimestamp_negative_nt_no_tz(self): + timestamp = -1572204340.6460679 + result = util.safe_fromtimestamp(timestamp, os_name="nt") + expected = util.windows_datetime_from_timestamp(timestamp, tz.tzlocal()) + self.assertEqual(result, expected) From 3b64e901844dddc7d20b9e2e4fa02f29d7502128 Mon Sep 17 00:00:00 2001 From: James Remeika Date: Sat, 2 Nov 2019 10:47:39 -0400 Subject: [PATCH 336/649] Formatter supports escape characters (#688) * Formatter support escape characters * Adds one more test for atomic matching * Removes logic around escaped brackets * Removes handling of escaped brackets * Adds comments on regex implementation * Trims trailing whitespace --- arrow/formatter.py | 9 +++++++- docs/index.rst | 12 +++++++++- tests/formatter_tests.py | 50 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/arrow/formatter.py b/arrow/formatter.py index ad1ac671e..80dd2c9bb 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -10,9 +10,13 @@ class DateTimeFormatter(object): + + # This pattern matches characters enclosed in square brackes 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 # TODO: test against full timezone DB _FORMAT_RE = re.compile( - r"(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X)" + r"(\[(?:(?=(?P[^]]))(?P=literal))*\]|YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X)" ) def __init__(self, locale="en_us"): @@ -25,6 +29,9 @@ def format(cls, dt, fmt): def _format_token(self, dt, token): + if token and token.startswith("[") and token.endswith("]"): + return token[1:-1] + if token == "YYYY": return self.locale.year_full(dt.year) if token == "YY": diff --git a/docs/index.rst b/docs/index.rst index ca07c34cd..7b92f118c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -381,7 +381,7 @@ Use the following tokens in parsing and formatting. Note that they're not the s Escaping Formats ~~~~~~~~~~~~~~~~ -Tokens, phrases, and regular expressions in a format string can be escaped when parsing by enclosing them within square brackets. +Tokens, phrases, and regular expressions in a format string can be escaped when parsing and formatting by enclosing them within square brackets. Tokens & Phrases ++++++++++++++++ @@ -390,17 +390,27 @@ Any `token `_ or phrase can be escaped as follows: .. code-block:: python + >>> formatter = arrow.formatter.DateTimeFormatter() + >>> dt = datetime(2018, 3, 9, 8, 40) + + >>> fmt = "YYYY-MM-DD h [h] m" >>> arrow.get("2018-03-09 8 h 40", fmt) + >>> formatter.format(dt, fmt) + u'2018-03-09 8 h 40' >>> fmt = "YYYY-MM-DD h [hello] m" >>> arrow.get("2018-03-09 8 hello 40", fmt) + >>> formatter.format(dt, fmt) + u'2018-03-09 8 hello 40' >>> fmt = "YYYY-MM-DD h [hello world] m" >>> arrow.get("2018-03-09 8 hello world 40", fmt) + >>> formatter.format(dt, fmt) + u'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". diff --git a/tests/formatter_tests.py b/tests/formatter_tests.py index 191be19a4..848ac260a 100644 --- a/tests/formatter_tests.py +++ b/tests/formatter_tests.py @@ -147,3 +147,53 @@ def test_nonsense(self): dt = datetime(2012, 1, 1, 11) self.assertEqual(self.formatter._format_token(dt, None), None) self.assertEqual(self.formatter._format_token(dt, "NONSENSE"), None) + + def test_escape(self): + + self.assertEqual( + self.formatter.format( + datetime(2015, 12, 10, 17, 9), "MMMM D, YYYY [at] h:mma" + ), + "December 10, 2015 at 5:09pm", + ) + + self.assertEqual( + self.formatter.format( + datetime(2015, 12, 10, 17, 9), "[MMMM] M D, YYYY [at] h:mma" + ), + "MMMM 12 10, 2015 at 5:09pm", + ) + + self.assertEqual( + self.formatter.format( + datetime(1990, 11, 25), + "[It happened on] MMMM Do [in the year] YYYY [a long time ago]", + ), + "It happened on November 25th in the year 1990 a long time ago", + ) + + self.assertEqual( + self.formatter.format( + datetime(1990, 11, 25), + "[It happened on] MMMM Do [in the][ year] YYYY [a long time ago]", + ), + "It happened on November 25th in the year 1990 a long time ago", + ) + + self.assertEqual( + self.formatter.format( + datetime(1, 1, 1), "[I'm][ entirely][ escaped,][ weee!]" + ), + "I'm entirely escaped, weee!", + ) + + # Special RegEx characters + self.assertEqual( + self.formatter.format( + datetime(2017, 12, 31, 2, 0), "MMM DD, YYYY |^${}().*+?<>-& h:mm A" + ), + "Dec 31, 2017 |^${}().*+?<>-& 2:00 AM", + ) + + # Escaping is atomic: brackets inside brackets are treated litterally + self.assertEqual(self.formatter.format(datetime(1, 1, 1), "[[[ ]]"), "[[ ]") From 065e96488d9db0e0d6859a9d9530ee9329da4d13 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 2 Nov 2019 15:24:09 +0000 Subject: [PATCH 337/649] Tweak docs (#707) --- docs/index.rst | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 7b92f118c..16ffb347d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -390,27 +390,23 @@ Any `token `_ or phrase can be escaped as follows: .. code-block:: python - >>> formatter = arrow.formatter.DateTimeFormatter() - >>> dt = datetime(2018, 3, 9, 8, 40) - - >>> fmt = "YYYY-MM-DD h [h] m" - >>> arrow.get("2018-03-09 8 h 40", fmt) + >>> arw = arrow.get("2018-03-09 8 h 40", fmt) - >>> formatter.format(dt, fmt) - u'2018-03-09 8 h 40' + >>> arw.format(fmt) + '2018-03-09 8 h 40' >>> fmt = "YYYY-MM-DD h [hello] m" - >>> arrow.get("2018-03-09 8 hello 40", fmt) + >>> arw = arrow.get("2018-03-09 8 hello 40", fmt) - >>> formatter.format(dt, fmt) - u'2018-03-09 8 hello 40' + >>> arw.format(fmt) + '2018-03-09 8 hello 40' >>> fmt = "YYYY-MM-DD h [hello world] m" - >>> arrow.get("2018-03-09 8 hello world 40", fmt) + >>> arw = arrow.get("2018-03-09 8 hello world 40", fmt) - >>> formatter.format(dt, fmt) - u'2018-03-09 8 hello world 40' + >>> 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". From 40eb90b8e5f2a548c8c10de92f8710959d87b70f Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Sat, 2 Nov 2019 15:21:30 +0000 Subject: [PATCH 338/649] Bump version to 0.15.3 and update CHANGELOG --- CHANGELOG.rst | 27 +++++++++++++++++++++++++++ arrow/_version.py | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9265b151a..d341aa1b6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,33 @@ Changelog ========= +0.15.3 (2019-11-02) +------------------- + +- [NEW] ``factory.get()`` can now create from a ISO calendar tuple, for example: + +.. code-block:: python + + >>> arrow.get((2013, 18, 7)) + + +- [NEW] Added a new token ``x`` to allow parsing of timestamps in milliseconds and microseconds. +- [NEW] Formatting now supports escaping of characters using the same syntax as parsing, for example: + +.. code-block:: python + + >>> formatter = arrow.formatter.DateTimeFormatter() + >>> dt = datetime(2018, 3, 9, 8, 40) + >>> fmt = "YYYY-MM-DD h [h] m" + >>> formatter.format(dt, fmt) + '2018-03-09 8 h 40' + +- [NEW] Added ``humanize`` week granularity translations for Chinese, Spanish and Vietnamese. +- [CHANGE] Added ParserError to module exports. +- [FIX] Added support for midnight at end of day, see `#703 `_ for details. +- [FIX] Create Travis build for macOS. +- [FIX] Test parsing and formatting against full tz database. + 0.15.2 (2019-09-14) ------------------- diff --git a/arrow/_version.py b/arrow/_version.py index c0d499920..8066fa278 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.15.2" +__version__ = "0.15.3" From 279fefed6c4243d66ec27bb55e263ec2200a729a Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Sat, 2 Nov 2019 15:38:42 +0000 Subject: [PATCH 339/649] Tweak wording --- CHANGELOG.rst | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d341aa1b6..5c7f301fa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,29 +4,28 @@ Changelog 0.15.3 (2019-11-02) ------------------- -- [NEW] ``factory.get()`` can now create from a ISO calendar tuple, for example: +- [NEW] ``factory.get()`` can now create arrow objects from a ISO calendar tuple, for example: .. code-block:: python >>> arrow.get((2013, 18, 7)) -- [NEW] Added a new token ``x`` to allow parsing of timestamps in milliseconds and microseconds. +- [NEW] Added a new token ``x`` to allow parsing of integer timestamps with milliseconds and microseconds. - [NEW] Formatting now supports escaping of characters using the same syntax as parsing, for example: .. code-block:: python - >>> formatter = arrow.formatter.DateTimeFormatter() - >>> dt = datetime(2018, 3, 9, 8, 40) + >>> arw = arrow.now() >>> fmt = "YYYY-MM-DD h [h] m" - >>> formatter.format(dt, fmt) - '2018-03-09 8 h 40' + >>> arw.format(fmt) + '2019-11-02 3 h 32' - [NEW] Added ``humanize`` week granularity translations for Chinese, Spanish and Vietnamese. -- [CHANGE] Added ParserError to module exports. -- [FIX] Added support for midnight at end of day, see `#703 `_ for details. -- [FIX] Create Travis build for macOS. -- [FIX] Test parsing and formatting against full tz database. +- [CHANGE] Added ``ParserError`` to module exports. +- [FIX] Added support for midnight at end of day. See `#703 `_ for details. +- [INTERNAL] Created Travis build for macOS. +- [INTERNAL] Test parsing and formatting against full timezone database. 0.15.2 (2019-09-14) ------------------- From 1694d646691faee594434fa3d0baa2f1ee10095c Mon Sep 17 00:00:00 2001 From: Nicholas Bollweg Date: Sat, 2 Nov 2019 20:05:04 -0400 Subject: [PATCH 340/649] revert #679 renaming of MANIFEST[.in] for #710 --- MANIFEST => MANIFEST.in | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename MANIFEST => MANIFEST.in (100%) diff --git a/MANIFEST b/MANIFEST.in similarity index 100% rename from MANIFEST rename to MANIFEST.in From 101b10170cd96638365a0c3f7141aeb4948b1145 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 2 Nov 2019 22:49:04 -0400 Subject: [PATCH 341/649] Increment version and add to changelog --- CHANGELOG.rst | 5 +++++ arrow/_version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5c7f301fa..2705c89d9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog ========= +0.15.4 (2019-11-02) +------------------- + +- [FIX] Fixed an issue that caused package installs to fail on Conda Forge. + 0.15.3 (2019-11-02) ------------------- diff --git a/arrow/_version.py b/arrow/_version.py index 8066fa278..8782a8b49 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.15.3" +__version__ = "0.15.4" From e67c810ba6252b57d0b4504a68c7fd4cf8625f1f Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Tue, 5 Nov 2019 12:37:47 +0200 Subject: [PATCH 342/649] Update backports.functools_lru_cache from 1.5.0 to 1.6.1 (#713) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 46d326fd9..8281e8e91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -backports.functools_lru_cache==1.5.0; python_version == "2.7" +backports.functools_lru_cache==1.6.1; python_version == "2.7" chai==1.1.2 dateparser==0.7.* mock==3.0.* From eed2f9dafce95d253a88fd87ecc5066554187f1c Mon Sep 17 00:00:00 2001 From: kkoziara Date: Thu, 21 Nov 2019 09:59:31 +0100 Subject: [PATCH 343/649] Reverted windows_datetime_from_timestamp to use local timezone by default. Removed os_name param from safe_fromtimestamp and safe_fromutctimestamp. Fixed tests and typos. --- arrow/util.py | 39 +++++++++++++++++++-------------------- tests/parser_tests.py | 31 +++++++++++++------------------ tests/util_tests.py | 17 +++++------------ 3 files changed, 37 insertions(+), 50 deletions(-) diff --git a/arrow/util.py b/arrow/util.py index d9e800e12..cf3bf5bba 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -3,8 +3,8 @@ import datetime import math -import os import time +from os import name as os_name import dateutil @@ -32,36 +32,35 @@ def windows_datetime_from_timestamp(timestamp, tz=None): """Computes datetime from timestamp. Supports negative timestamps on Windows platform.""" sec_frac, sec = math.modf(timestamp) dt = datetime.datetime(1970, 1, 1, tzinfo=dateutil.tz.tzutc()) + datetime.timedelta( - seconds=sec, microseconds=sec_frac * 1e6 + seconds=sec, microseconds=sec_frac * 1000000 ) - if tz is not None: - if tz == dateutil.tz.tzlocal(): - # because datetime.astimezone does not work on Windows for tzlocal() and dates before the 1970-01-01 - # take timestamp from appropriate time of the year, because of daylight saving time changes - ts = time.mktime(dt.replace(year=1970).timetuple()) - dt += datetime.datetime.fromtimestamp( - ts - ) - datetime.datetime.utcfromtimestamp(ts) - dt = dt.replace(tzinfo=dateutil.tz.tzlocal()) - else: - dt = dt.astimezone(tz) + if tz is None: + tz = dateutil.tz.tzlocal() + + if tz == dateutil.tz.tzlocal(): + # because datetime.astimezone does not work on Windows for tzlocal() and dates before the 1970-01-01 + # take timestamp from appropriate time of the year, because of daylight saving time changes + ts = time.mktime(dt.replace(year=1970).timetuple()) + dt += datetime.datetime.fromtimestamp(ts) - datetime.datetime.utcfromtimestamp( + ts + ) + dt = dt.replace(tzinfo=dateutil.tz.tzlocal()) + else: + dt = dt.astimezone(tz) return dt -def safe_utcfromtimestamp(timestamp, os_name=os.name): - """ datetime.utcfromtimestamp alternative which supports negatvie timestamps on Windows platform.""" +def safe_utcfromtimestamp(timestamp): + """ datetime.utcfromtimestamp alternative which supports negative timestamps on Windows platform.""" if os_name == "nt" and timestamp < 0: return windows_datetime_from_timestamp(timestamp, dateutil.tz.tzutc()) else: return datetime.datetime.utcfromtimestamp(timestamp) -def safe_fromtimestamp(timestamp, tz=None, os_name=os.name): - """ datetime.fromtimestamp alternative which supports negatvie timestamps on Windows platform.""" +def safe_fromtimestamp(timestamp, tz=None): + """ datetime.fromtimestamp alternative which supports negative timestamps on Windows platform.""" if os_name == "nt" and timestamp < 0: - if tz is None: - # because datetime.fromtimestamp default is local time - tz = dateutil.tz.tzlocal() return windows_datetime_from_timestamp(timestamp, tz) else: return datetime.datetime.fromtimestamp(timestamp, tz) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 798c04de4..baa82da51 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import calendar -import os import time from datetime import datetime @@ -11,7 +10,7 @@ from dateutil import tz from dateutil.zoneinfo import get_zonefile_instance -from arrow import parser +from arrow import parser, util from arrow.constants import MAX_TIMESTAMP_US from arrow.parser import DateTimeParser, ParserError, ParserMatchError @@ -234,23 +233,19 @@ def test_parse_timestamp(self): self.parser.parse("{:f}123456".format(float_timestamp), "X"), self.expected ) - # NOTE: negative timestamps cannot be handled by datetime on Window - # Must use timedelta to handle them. ref: https://stackoverflow.com/questions/36179914 - if os.name != "nt": - # regression test for issue #662 - negative_int_timestamp = -int_timestamp - self.expected = datetime.fromtimestamp(negative_int_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:d}".format(negative_int_timestamp), "X"), - self.expected, - ) + # regression test for issue #662 + negative_int_timestamp = -int_timestamp + self.expected = util.safe_fromtimestamp(negative_int_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:d}".format(negative_int_timestamp), "X"), self.expected + ) - negative_float_timestamp = -float_timestamp - self.expected = datetime.fromtimestamp(negative_float_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:f}".format(negative_float_timestamp), "X"), - self.expected, - ) + negative_float_timestamp = -float_timestamp + self.expected = util.safe_fromtimestamp(negative_float_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:f}".format(negative_float_timestamp), "X"), + self.expected, + ) # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will # break cases like "15 Jul 2000" and a format list (see issue #447) diff --git a/tests/util_tests.py b/tests/util_tests.py index f2bc9d5a6..980a0a93e 100644 --- a/tests/util_tests.py +++ b/tests/util_tests.py @@ -4,6 +4,7 @@ from chai import Chai from dateutil import tz +from mock import patch from arrow import util @@ -80,26 +81,18 @@ def test_safe_fromtimestamp_negative(self): ) self.assertEqual(result, expected) + @patch.object(util, "os_name", "nt") def test_safe_utcfromtimestamp_negative_nt(self): timestamp = -1572204340.6460679 - result = util.safe_utcfromtimestamp(timestamp, os_name="nt").replace( - tzinfo=tz.tzutc() - ) + result = util.safe_utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) expected = datetime(1920, 3, 7, 4, 34, 19, 353932, tzinfo=tz.tzutc()) self.assertEqual(result, expected) + @patch.object(util, "os_name", "nt") def test_safe_fromtimestamp_negative_nt(self): timestamp = -1572204340.6460679 - result = util.safe_fromtimestamp( - timestamp, tz.gettz("Europe/Paris"), os_name="nt" - ) + result = util.safe_fromtimestamp(timestamp, tz.gettz("Europe/Paris")) expected = datetime( 1920, 3, 7, 5, 34, 19, 353932, tzinfo=tz.gettz("Europe/Paris") ) self.assertEqual(result, expected) - - def test_safe_fromtimestamp_negative_nt_no_tz(self): - timestamp = -1572204340.6460679 - result = util.safe_fromtimestamp(timestamp, os_name="nt") - expected = util.windows_datetime_from_timestamp(timestamp, tz.tzlocal()) - self.assertEqual(result, expected) From 52366e9d13a894ddd834f502298e0004a2b42d12 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Thu, 21 Nov 2019 16:07:32 +0000 Subject: [PATCH 344/649] Allow factory.get() to construct from a timestamp and timezone --- arrow/factory.py | 4 ++++ tests/factory_tests.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/arrow/factory.py b/arrow/factory.py index b6d78b64d..c9310cd24 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -168,6 +168,10 @@ def get(self, *args, **kwargs): if arg is None: return self.type.utcnow() + # try (int, float) -> from timestamp with tz + elif not isstr(arg) and is_timestamp(arg) and tz is not None: + return self.type.fromtimestamp(arg, tzinfo=tz) + # try (int, float) -> utc, from timestamp. elif not isstr(arg) and is_timestamp(arg): return self.type.utcfromtimestamp(arg) diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 98d9eca88..a673559cd 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -73,6 +73,16 @@ def test_one_arg_timestamp(self): with self.assertRaises((OverflowError, ValueError)): self.factory.get(timestamp) + def test_one_arg_timestamp_with_tzinfo(self): + + int_timestamp = int(time.time()) + timestamp_dt = datetime.utcfromtimestamp(int_timestamp).astimezone( + tz.gettz("US/Pacific") + ) + timezone = tz.gettz("US/Pacific") + + assertDtEqual(self.factory.get(int_timestamp, tzinfo=timezone), timestamp_dt) + def test_one_arg_arrow(self): arw = self.factory.utcnow() From b43cd2b341d64034115292b9572763c6980e6fd3 Mon Sep 17 00:00:00 2001 From: kkoziara Date: Thu, 21 Nov 2019 20:16:05 +0100 Subject: [PATCH 345/649] Updated the methods params descriptions. --- arrow/arrow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 5bfe24012..303ec12d4 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -362,7 +362,7 @@ def span_range(cls, frame, start, end, tz=None, limit=None, bounds="[)"): ``start``'s timezone, or UTC if ``start`` is naive. :param limit: (optional) A maximum number of tuples to return. :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies - whether to include or exclude the start and end values in the span. '(' excludes + whether to include or exclude the start and end values in each span in the range. '(' excludes the start, '[' includes the start, ')' excludes the end, and ']' includes the end. If the bounds are not specified, the default bound '[)' is used. @@ -416,7 +416,7 @@ def interval(cls, frame, start, end, interval=1, tz=None, bounds="[)"): :param interval: (optional) Time interval for the given time frame. :param tz: (optional) A timezone expression. Defaults to UTC. :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies - whether to include or exclude the start and end values in the span. '(' excludes + whether to include or exclude the start and end values in the intervals. '(' excludes the start, '[' includes the start, ')' excludes the end, and ']' includes the end. If the bounds are not specified, the default bound '[)' is used. From 8304dba4c4d87e6b17c8301ad287a0e749abb5c9 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Thu, 21 Nov 2019 20:09:10 +0000 Subject: [PATCH 346/649] Correct error in tests --- tests/factory_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/factory_tests.py b/tests/factory_tests.py index a673559cd..8467fcc28 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -76,7 +76,7 @@ def test_one_arg_timestamp(self): def test_one_arg_timestamp_with_tzinfo(self): int_timestamp = int(time.time()) - timestamp_dt = datetime.utcfromtimestamp(int_timestamp).astimezone( + timestamp_dt = datetime.fromtimestamp(int_timestamp, tz=tz.tzutc()).astimezone( tz.gettz("US/Pacific") ) timezone = tz.gettz("US/Pacific") From f434f7a6b54e41106d20991a176994c6a62ad9e9 Mon Sep 17 00:00:00 2001 From: andrewchouman Date: Sun, 24 Nov 2019 21:27:08 -0500 Subject: [PATCH 347/649] Fixed issue #701 --- arrow/parser.py | 17 +++++++++++------ docs/index.rst | 12 ++++++++++++ tests/parser_tests.py | 29 ++++++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index a13b7152e..2b7ef812f 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -218,6 +218,7 @@ def parse(self, datetime_string, fmt): fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt) match = fmt_pattern_re.search(datetime_string) + if match is None: raise ParserMatchError( "Failed to match '{}' when parsing '{}'".format(fmt, datetime_string) @@ -292,12 +293,16 @@ def _generate_pattern_re(self, fmt): # and time string in a natural language sentence. Therefore, searching # for a string of the form YYYY-MM-DD in "blah 1998-09-12 blah" will # work properly. - # Reference: https://stackoverflow.com/q/14232931/3820660 - starting_word_boundary = r"(?\s])(\b|^)" + ending_punctuation_bound = r"(?=[,.;:?!\"'`\[\]{}()<>]?(?!\S))" + bounded_fmt_pattern = r"{}{}{}".format(starting_punctuation_bound, + final_fmt_pattern, + ending_punctuation_bound) return tokens, re.compile(bounded_fmt_pattern, flags=re.IGNORECASE) diff --git a/docs/index.rst b/docs/index.rst index 16ffb347d..d066f7862 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -212,6 +212,7 @@ Support for a growing number of locales (see ``locales.py`` for supported langua .. code-block:: python + >>> future = arrow.utcnow().shift(hours=1) >>> future.humanize(a, locale='ru') 'через 2 час(а,ов)' @@ -427,6 +428,17 @@ You can also escape regular expressions by enclosing them within square brackets >>> arrow.get("Mon Sep 08 16:41:45 2014", fmt) +Punctuation +~~~~~~~~~~~ + +You're date formats can be precede and/or be preceded by one character of +punctuation from the list: :code:`, . ; : ? ! " ' ` [ ] { } ( ) < >` + +.. code-block:: python + + >>> arrow.get("Tomorrow (2019-10-31) is Halloween!", "YYYY-MM-DD") + + API Guide --------- diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 93eefc5c5..93a5e7956 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -472,7 +472,10 @@ def test_parse_with_extra_words_at_start_and_end_invalid(self): def test_parse_with_extra_words_at_start_and_end_valid(self): # Spaces surrounding the parsable date are ok because we - # allow the parsing of natural language input + # allow the parsing of natural language input. Additionally, a single + # character of specific punctuation before or after the date is okay. + # See docs for full list of valid punctuation. + self.assertEqual( self.parser.parse("blah 2016 blah", "YYYY"), datetime(2016, 1, 1) ) @@ -527,6 +530,30 @@ def test_parse_with_extra_words_at_start_and_end_valid(self): datetime(2016, 5, 16, 4, 5, 6, 789120), ) + self.assertEqual( + self.parser.parse( + "Meet me at my house on the my birthday (2019-24-11)", + "YYYY-DD-MM" + ), + datetime(2019, 11, 24) + ) + + self.assertEqual( + self.parser.parse( + "Monday, 9. September 2019, 16:15-20:00", + "dddd, D. MMMM YYYY" + ), + datetime(2019, 9, 9) + ) + + self.assertEqual( + self.parser.parse( + "A date is 11.11.2011.", + "DD.MM.YYYY" + ), + datetime(2011, 11, 11) + ) + def test_parse_with_leading_and_trailing_whitespace(self): self.assertEqual(self.parser.parse(" 2016", "YYYY"), datetime(2016, 1, 1)) From f3c301573c5f27655864566ee35966137aba127c Mon Sep 17 00:00:00 2001 From: andrewchouman Date: Sun, 24 Nov 2019 21:43:19 -0500 Subject: [PATCH 348/649] Fixed a small redundancy in the regex --- arrow/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/parser.py b/arrow/parser.py index 2b7ef812f..66f6a19c0 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -297,7 +297,7 @@ def _generate_pattern_re(self, fmt): # "1998-09-12," is permitted. For the full list of valid punctuation, # see the documentation. - starting_punctuation_bound = r"(?\s])(\b|^)" ending_punctuation_bound = r"(?=[,.;:?!\"'`\[\]{}()<>]?(?!\S))" bounded_fmt_pattern = r"{}{}{}".format(starting_punctuation_bound, From ce9e5b420cb3f59c2fad90fd052330539b7ea93a Mon Sep 17 00:00:00 2001 From: andrewchouman Date: Mon, 25 Nov 2019 12:16:25 -0500 Subject: [PATCH 349/649] Fixed linting failures --- arrow/parser.py | 11 ++++++----- docs/index.rst | 2 +- tests/parser_tests.py | 17 ++++++----------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 66f6a19c0..922b423a6 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -297,12 +297,13 @@ def _generate_pattern_re(self, fmt): # "1998-09-12," is permitted. For the full list of valid punctuation, # see the documentation. - starting_punctuation_bound = r"(?\s])(\b|^)" + starting_punctuation_bound = ( + r"(?\s])(\b|^)" + ) ending_punctuation_bound = r"(?=[,.;:?!\"'`\[\]{}()<>]?(?!\S))" - bounded_fmt_pattern = r"{}{}{}".format(starting_punctuation_bound, - final_fmt_pattern, - ending_punctuation_bound) + bounded_fmt_pattern = r"{}{}{}".format( + starting_punctuation_bound, final_fmt_pattern, ending_punctuation_bound + ) return tokens, re.compile(bounded_fmt_pattern, flags=re.IGNORECASE) diff --git a/docs/index.rst b/docs/index.rst index d066f7862..9451e2ad1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -432,7 +432,7 @@ Punctuation ~~~~~~~~~~~ You're date formats can be precede and/or be preceded by one character of -punctuation from the list: :code:`, . ; : ? ! " ' ` [ ] { } ( ) < >` +punctuation from the list: :code:``, . ; : ? ! " ' \` [ ] { } ( ) < >`` .. code-block:: python diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 93a5e7956..2c5382809 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -532,26 +532,21 @@ def test_parse_with_extra_words_at_start_and_end_valid(self): self.assertEqual( self.parser.parse( - "Meet me at my house on the my birthday (2019-24-11)", - "YYYY-DD-MM" + "Meet me at my house on the my birthday (2019-24-11)", "YYYY-DD-MM" ), - datetime(2019, 11, 24) + datetime(2019, 11, 24), ) self.assertEqual( self.parser.parse( - "Monday, 9. September 2019, 16:15-20:00", - "dddd, D. MMMM YYYY" + "Monday, 9. September 2019, 16:15-20:00", "dddd, D. MMMM YYYY" ), - datetime(2019, 9, 9) + datetime(2019, 9, 9), ) self.assertEqual( - self.parser.parse( - "A date is 11.11.2011.", - "DD.MM.YYYY" - ), - datetime(2011, 11, 11) + self.parser.parse("A date is 11.11.2011.", "DD.MM.YYYY"), + datetime(2011, 11, 11), ) def test_parse_with_leading_and_trailing_whitespace(self): From 6b70d809705696da1d73c153343bb7759f287412 Mon Sep 17 00:00:00 2001 From: Howard Willard Date: Tue, 26 Nov 2019 10:18:54 -0500 Subject: [PATCH 350/649] Added support for multiple granularities in humanize function --- arrow/arrow.py | 67 ++++++++++++++++++++++++++++++++++++++------ arrow/locales.py | 37 ++++++++++++++++++++++++ setup.cfg | 2 +- tests/arrow_tests.py | 60 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 9 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index d752e393a..2e5b219db 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -13,6 +13,7 @@ from datetime import tzinfo as dt_tzinfo from math import trunc +import six from dateutil import tz as dateutil_tz from dateutil.relativedelta import relativedelta @@ -56,6 +57,12 @@ class Arrow(object): _ATTRS = ["year", "month", "day", "hour", "minute", "second", "microsecond"] _ATTRS_PLURAL = ["{}s".format(a) for a in _ATTRS] _MONTHS_PER_QUARTER = 3 + _SECS_PER_MINUTE = float(60) + _SECS_PER_HOUR = float(3600) # 60 * 60 + _SECS_PER_DAY = float(86400) # 60 * 60 * 24 + _SECS_PER_WEEK = float(604800) # 60 * 60 * 24 * 7 + _SECS_PER_MONTH = float(2635200) # 60 * 60 * 24 * 30.5 + _SECS_PER_YEAR = float(31557600) # 60 * 60 * 24 * 365.25 def __init__( self, year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None @@ -844,7 +851,8 @@ def humanize( Defaults to now in the current :class:`Arrow ` object's timezone. :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'. :param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part. - :param granularity: (optional) defines the precision of the output. Set it to strings 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'. + :param granularity: (optional) defines the precision of the output. Set it to strings 'second', 'minute', + 'hour', 'day', 'week', 'month' or 'year' or a list of any combination of these strings Usage:: @@ -877,6 +885,9 @@ def humanize( else: raise TypeError() + if isinstance(granularity, list) and len(granularity) == 1: + granularity = granularity[0] + delta = int(round(util.total_seconds(self._datetime - dt))) sign = -1 if delta < 0 else 1 diff = abs(delta) @@ -937,23 +948,23 @@ def humanize( years = sign * int(max(delta / 31536000, 2)) return locale.describe("years", years, only_distance=only_distance) - else: + elif isinstance(granularity, six.string_types): if granularity == "second": delta = sign * delta if abs(delta) < 2: return locale.describe("now", only_distance=only_distance) elif granularity == "minute": - delta = sign * delta / float(60) + delta = sign * delta / self._SECS_PER_MINUTE elif granularity == "hour": - delta = sign * delta / float(60 * 60) + delta = sign * delta / self._SECS_PER_HOUR elif granularity == "day": - delta = sign * delta / float(60 * 60 * 24) + delta = sign * delta / self._SECS_PER_DAY elif granularity == "week": - delta = sign * delta / float(60 * 60 * 24 * 7) + delta = sign * delta / self._SECS_PER_WEEK elif granularity == "month": - delta = sign * delta / float(60 * 60 * 24 * 30.5) + delta = sign * delta / self._SECS_PER_MONTH elif granularity == "year": - delta = sign * delta / float(60 * 60 * 24 * 365.25) + delta = sign * delta / self._SECS_PER_YEAR else: raise AttributeError( "Invalid level of granularity. Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'" @@ -962,6 +973,46 @@ def humanize( if trunc(abs(delta)) != 1: granularity += "s" return locale.describe(granularity, delta, only_distance=only_distance) + + else: + timeframes = [] + if "year" in granularity: + years = sign * delta / self._SECS_PER_YEAR + delta -= sign * trunc(years) * self._SECS_PER_YEAR + timeframes.append(["year", years]) + if "month" in granularity: + months = sign * delta / self._SECS_PER_MONTH + delta -= sign * trunc(months) * self._SECS_PER_MONTH + timeframes.append(["month", months]) + if "week" in granularity: + weeks = sign * delta / self._SECS_PER_WEEK + delta -= sign * trunc(weeks) * self._SECS_PER_WEEK + timeframes.append(["week", weeks]) + if "day" in granularity: + days = sign * delta / self._SECS_PER_DAY + delta -= sign * trunc(days) * self._SECS_PER_DAY + timeframes.append(["day", days]) + if "hour" in granularity: + hours = sign * delta / self._SECS_PER_HOUR + delta -= sign * trunc(hours) * self._SECS_PER_HOUR + timeframes.append(["hour", hours]) + if "minute" in granularity: + minutes = sign * delta / self._SECS_PER_MINUTE + delta -= sign * trunc(minutes) * self._SECS_PER_MINUTE + timeframes.append(["minute", minutes]) + if "second" in granularity: + seconds = sign * delta + timeframes.append(["second", seconds]) + if len(timeframes) < len(granularity): + raise AttributeError( + "Invalid level of granularity. Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'" + ) + for index in range(len(timeframes)): + gran, delta = timeframes[index] + if trunc(abs(delta)) != 1: + timeframes[index][0] += "s" + return locale.describe_multi(timeframes, only_distance=only_distance) + except KeyError as e: raise ValueError( "Humanization of the {} granularity is not currently translated in the '{}' locale. Please consider making a contribution to this locale.".format( diff --git a/arrow/locales.py b/arrow/locales.py index 1a50662e9..23db2762f 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -51,6 +51,7 @@ class Locale(object): past = None future = None + and_word = None month_names = [] month_abbreviations = [] @@ -78,6 +79,27 @@ def describe(self, timeframe, delta=0, only_distance=False): return humanized + def describe_multi(self, timeframes, only_distance=False): + """ Describes a delta within multiple timeframes in plain language. + + :param timeframes: a list of string, quantity pairs each representing a timeframe and delta. + :param only_distance: return only distance eg: "2 hours 11 seconds" without "in" or "ago" keywords + """ + + humanized = "" + for index in range(len(timeframes)): + timeframe, delta = timeframes[index] + humanized += self._format_timeframe(timeframe, delta) + if index == len(timeframes) - 2 and self.and_word: + humanized += " " + self.and_word + " " + elif index < len(timeframes) - 1: + humanized += " " + + if not only_distance: + humanized = self._format_relative(humanized, timeframe, delta) + + return humanized + def day_name(self, day): """ Returns the day name for a specified day of the week. @@ -200,6 +222,7 @@ class EnglishLocale(Locale): past = "{0} ago" future = "in {0}" + and_word = "and" timeframes = { "now": "just now", @@ -295,6 +318,7 @@ class ItalianLocale(Locale): names = ["it", "it_it"] past = "{0} fa" future = "tra {0}" + and_word = "e" timeframes = { "now": "adesso", @@ -364,6 +388,7 @@ class SpanishLocale(Locale): names = ["es", "es_es"] past = "hace {0}" future = "en {0}" + and_word = "y" timeframes = { "now": "ahora", @@ -437,6 +462,7 @@ class FrenchLocale(Locale): names = ["fr", "fr_fr"] past = "il y a {0}" future = "dans {0}" + and_word = "et" timeframes = { "now": "maintenant", @@ -514,6 +540,7 @@ class GreekLocale(Locale): past = "{0} πριν" future = "σε {0}" + and_word = "και" timeframes = { "now": "τώρα", @@ -639,6 +666,7 @@ class SwedishLocale(Locale): past = "för {0} sen" future = "om {0}" + and_word = "och" timeframes = { "now": "just nu", @@ -1527,6 +1555,7 @@ class DeutschBaseLocale(Locale): past = "vor {0}" future = "in {0}" + and_word = "und" timeframes = { "now": "gerade eben", @@ -1778,6 +1807,7 @@ class PortugueseLocale(Locale): past = "há {0}" future = "em {0}" + and_word = "e" timeframes = { "now": "agora", @@ -2495,6 +2525,7 @@ class DanishLocale(Locale): past = "for {0} siden" future = "efter {0}" + and_word = "og" timeframes = { "now": "lige nu", @@ -2802,6 +2833,7 @@ class SlovakLocale(Locale): past = "Pred {0}" future = "O {0}" + and_word = "a" month_names = [ "", @@ -3101,6 +3133,7 @@ class CatalanLocale(Locale): names = ["ca", "ca_es", "ca_ad", "ca_fr", "ca_it"] past = "Fa {0}" future = "En {0}" + and_word = "i" timeframes = { "now": "Ara mateix", @@ -3685,6 +3718,7 @@ class RomanianLocale(Locale): past = "{0} în urmă" future = "peste {0}" + and_word = "și" timeframes = { "now": "acum", @@ -3750,6 +3784,7 @@ class SlovenianLocale(Locale): past = "pred {0}" future = "čez {0}" + and_word = "in" timeframes = { "now": "zdaj", @@ -3820,6 +3855,7 @@ class IndonesianLocale(Locale): past = "{0} yang lalu" future = "dalam {0}" + and_word = "dan" timeframes = { "now": "baru saja", @@ -3957,6 +3993,7 @@ class EstonianLocale(Locale): past = "{0} tagasi" future = "{0} pärast" + and_word = "ja" timeframes = { "now": {"past": "just nüüd", "future": "just nüüd"}, diff --git a/setup.cfg b/setup.cfg index 08cae3ddf..2ab246006 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ ignore = E203,E501,W503 line_length = 88 multi_line_output = 3 include_trailing_comma = true -known_third_party = chai,dateparser,dateutil,mock,pytz,setuptools,simplejson +known_third_party = chai,dateparser,dateutil,mock,pytz,setuptools,simplejson,six [bdist_wheel] universal = 1 diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 4b26e6195..22587cb13 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1435,6 +1435,12 @@ def test_granularity(self): self.assertEqual( later105.humanize(self.now, granularity="month"), "in 0 months" ) + self.assertEqual( + self.now.humanize(later105, granularity=["month"]), "0 months ago" + ) + self.assertEqual( + later105.humanize(self.now, granularity=["month"]), "in 0 months" + ) later106 = self.now.shift(seconds=3 * 10 ** 6) self.assertEqual(self.now.humanize(later106, granularity="day"), "34 days ago") @@ -1479,9 +1485,63 @@ def test_granularity(self): ), "3 years", ) + with self.assertRaises(AttributeError): self.now.humanize(later108, granularity="years") + def test_multiple_granularity(self): + self.assertEqual(self.now.humanize(granularity="second"), "just now") + self.assertEqual(self.now.humanize(granularity=["second"]), "just now") + self.assertEqual( + self.now.humanize(granularity=["year", "month", "day", "hour", "second"]), + "in 0 years 0 months 0 days 0 hours and seconds", + ) + + later4000 = self.now.shift(seconds=4000) + self.assertEqual( + later4000.humanize(self.now, granularity=["hour", "minute"]), + "in an hour and 6 minutes", + ) + self.assertEqual( + self.now.humanize(later4000, granularity=["hour", "minute"]), + "an hour and 6 minutes ago", + ) + self.assertEqual( + later4000.humanize( + self.now, granularity=["hour", "minute"], only_distance=True + ), + "an hour and 6 minutes", + ) + self.assertEqual( + later4000.humanize(self.now, granularity=["day", "hour", "minute"]), + "in 0 days an hour and 6 minutes", + ) + self.assertEqual( + self.now.humanize(later4000, granularity=["day", "hour", "minute"]), + "0 days an hour and 6 minutes ago", + ) + + later105 = self.now.shift(seconds=10 ** 5) + self.assertEqual( + self.now.humanize(later105, granularity=["hour", "day", "minute"]), + "a day 3 hours and 46 minutes ago", + ) + with self.assertRaises(AttributeError): + self.now.humanize(later105, granularity=["error", "second"]) + + later108onlydistance = self.now.shift(seconds=10 ** 8) + self.assertEqual( + self.now.humanize(later108onlydistance, granularity=["year"]), "3 years ago" + ) + self.assertEqual( + self.now.humanize(later108onlydistance, granularity=["month", "week"]), + "37 months and 4 weeks ago", + ) + self.assertEqual( + self.now.humanize(later108onlydistance, granularity=["year", "second"]), + "3 years and seconds ago", + ) + def test_seconds(self): later = self.now.shift(seconds=10) From 3cca6fbdb361e2ca8be54438a2135fdff81b851a Mon Sep 17 00:00:00 2001 From: Howard Willard Date: Tue, 26 Nov 2019 10:29:03 -0500 Subject: [PATCH 351/649] Multiple granularities logic simplification --- arrow/arrow.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 2e5b219db..b19017d19 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -978,27 +978,27 @@ def humanize( timeframes = [] if "year" in granularity: years = sign * delta / self._SECS_PER_YEAR - delta -= sign * trunc(years) * self._SECS_PER_YEAR + delta = delta % self._SECS_PER_YEAR timeframes.append(["year", years]) if "month" in granularity: months = sign * delta / self._SECS_PER_MONTH - delta -= sign * trunc(months) * self._SECS_PER_MONTH + delta = delta % self._SECS_PER_MONTH timeframes.append(["month", months]) if "week" in granularity: weeks = sign * delta / self._SECS_PER_WEEK - delta -= sign * trunc(weeks) * self._SECS_PER_WEEK + delta = delta % self._SECS_PER_WEEK timeframes.append(["week", weeks]) if "day" in granularity: days = sign * delta / self._SECS_PER_DAY - delta -= sign * trunc(days) * self._SECS_PER_DAY + delta = delta % self._SECS_PER_DAY timeframes.append(["day", days]) if "hour" in granularity: hours = sign * delta / self._SECS_PER_HOUR - delta -= sign * trunc(hours) * self._SECS_PER_HOUR + delta = delta % self._SECS_PER_HOUR timeframes.append(["hour", hours]) if "minute" in granularity: minutes = sign * delta / self._SECS_PER_MINUTE - delta -= sign * trunc(minutes) * self._SECS_PER_MINUTE + delta = delta % self._SECS_PER_MINUTE timeframes.append(["minute", minutes]) if "second" in granularity: seconds = sign * delta From 72e50b2da4f688244b360e92dd0f7333a1bac3d6 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 28 Nov 2019 14:01:27 -0500 Subject: [PATCH 352/649] Added PR and issue templates (#721) * Added PR and issue templates * Added space between emoji --- .github/ISSUE_TEMPLATE/bug_report.md | 29 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/documentation.md | 17 +++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 18 ++++++++++++++ .github/pull_request_template.md | 23 ++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/documentation.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/pull_request_template.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..34e139b59 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: "🐞 Bug Report" +about: Find a bug? Create a report to help us improve. +title: '' +labels: 'bug' +assignees: '' +--- + + + +## Issue Description + + + +## System Info + +- 🖥 **OS name and version**: +- 🐍 **Python version**: +- 🏹 **Arrow version**: diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 000000000..c6b85c8c5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,17 @@ +--- +name: "📚 Documentation" +about: Find errors or problems in the docs (https://arrow.readthedocs.io)? +title: '' +labels: 'documentation' +assignees: '' +--- + + + +## Issue Description + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..4202e812c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: "💡 Feature Request" +about: Have an idea for a new feature or improvement? +title: '' +labels: 'enhancement' +assignees: '' +--- + + + +## Feature Request + + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..552b9a972 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,23 @@ +## Pull Request Checklist + +Thank you for taking the time to improve Arrow! Before submitting your pull request, please check all *appropriate* boxes: + + +- [ ] 🧪 Added **tests** for changed code. +- [ ] 🛠️ All tests **pass** when run locally (run `tox` or `make test` to find out!). +- [ ] 📚 Updated **documentation** for changed code. +- [ ] ⏩ Code is **up-to-date** with the `master` branch. + +If you have *any* questions about your code changes or any of the points above, please submit your questions along with the pull request and we will try our best to help! + +## Description of Changes + + From fd72ecba52afb9b7f5da0e3a69320a83a4f48be5 Mon Sep 17 00:00:00 2001 From: andrewchouman Date: Sat, 30 Nov 2019 14:38:00 -0500 Subject: [PATCH 353/649] Made suggested changes from #720 --- arrow/parser.py | 14 ++++++++++---- docs/index.rst | 9 +++++++-- tests/factory_tests.py | 8 ++++++++ tests/parser_tests.py | 17 +++++++++++++++-- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 922b423a6..45008fa34 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -297,12 +297,18 @@ def _generate_pattern_re(self, fmt): # "1998-09-12," is permitted. For the full list of valid punctuation, # see the documentation. - starting_punctuation_bound = ( - r"(?\s])(\b|^)" + starting_word_boundary = ( + r"(?\s])" # This is the list of punctuation that is ok before the pattern (i.e. "It can't not be these characters before the pattern") + r"(\b|^)" # The \b is to block cases like 1201912 but allow 201912 for pattern YYYYMM. The ^ was necessary to allow a negative number through i.e. before epoch numbers + ) + ending_word_boundary = ( + r"(?=[\,\.\;\:\?\!\"\'\`\[\]\{\}\(\)\<\>]?" # Positive lookahead stating that these punctuation marks can appear after the pattern at most 1 time + r"(?!\S))" # Don't allow any non-whitespace character after the punctuation ) - ending_punctuation_bound = r"(?=[,.;:?!\"'`\[\]{}()<>]?(?!\S))" bounded_fmt_pattern = r"{}{}{}".format( - starting_punctuation_bound, final_fmt_pattern, ending_punctuation_bound + starting_word_boundary, final_fmt_pattern, ending_word_boundary ) return tokens, re.compile(bounded_fmt_pattern, flags=re.IGNORECASE) diff --git a/docs/index.rst b/docs/index.rst index 9451e2ad1..5a83d7496 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -431,14 +431,19 @@ You can also escape regular expressions by enclosing them within square brackets Punctuation ~~~~~~~~~~~ -You're date formats can be precede and/or be preceded by one character of -punctuation from the list: :code:``, . ; : ? ! " ' \` [ ] { } ( ) < >`` +Date formats may be fenced on either side by one punctuation character from the following list: :literal:`, . ; : ? ! " \` ' [ ] { } ( ) < >` .. code-block:: python >>> 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 + API Guide --------- diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 98d9eca88..9f7e67a8d 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -323,6 +323,14 @@ def test_locale(self): result._datetime, datetime(2010, 1, 1, 0, 0, 0, 0, tzinfo=tz.tzutc()) ) + # regression test for issue #701 + result = self.factory.get( + "Montag, 9. September 2019, 16:15-20:00", "dddd, D. MMMM YYYY", locale="de" + ) + self.assertEqual( + result._datetime, datetime(2019, 9, 9, 0, 0, 0, 0, tzinfo=tz.tzutc()) + ) + def test_locale_kwarg_only(self): res = self.factory.get(locale="ja") self.assertEqual(res.tzinfo, tz.tzutc()) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 2c5382809..c72da044b 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -530,11 +530,15 @@ def test_parse_with_extra_words_at_start_and_end_valid(self): datetime(2016, 5, 16, 4, 5, 6, 789120), ) + # regression test for issue #701 + # tests cases of a partial match surrounded by punctuation + # for the list of valid punctuation, see documentation + def test_parse_with_punctuation_fences(self): self.assertEqual( self.parser.parse( - "Meet me at my house on the my birthday (2019-24-11)", "YYYY-DD-MM" + "Meet me at my house on Halloween (2019-31-10)", "YYYY-DD-MM" ), - datetime(2019, 11, 24), + datetime(2019, 10, 31), ) self.assertEqual( @@ -549,6 +553,15 @@ def test_parse_with_extra_words_at_start_and_end_valid(self): datetime(2011, 11, 11), ) + with self.assertRaises(ParserMatchError): + self.parser.parse("11.11.2011.1 is not a valid date.", "DD.MM.YYYY") + + with self.assertRaises(ParserMatchError): + self.parser.parse( + "This date has too many punctuation marks following it (11.11.2011).", + "DD.MM.YYYY", + ) + def test_parse_with_leading_and_trailing_whitespace(self): self.assertEqual(self.parser.parse(" 2016", "YYYY"), datetime(2016, 1, 1)) From a8f34fc9bd803de6e96a964749d80c13dd571ed9 Mon Sep 17 00:00:00 2001 From: andrewchouman Date: Sat, 30 Nov 2019 14:46:07 -0500 Subject: [PATCH 354/649] Fixed a small comment formatting issue --- arrow/parser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index 45008fa34..379c68737 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -299,8 +299,7 @@ def _generate_pattern_re(self, fmt): starting_word_boundary = ( r"(?\s])" # This is the list of punctuation that is ok before the pattern (i.e. "It can't not be these characters before the pattern") + r"(?\s])" # This is the list of punctuation that is ok before the pattern (i.e. "It can't not be these characters before the pattern") r"(\b|^)" # The \b is to block cases like 1201912 but allow 201912 for pattern YYYYMM. The ^ was necessary to allow a negative number through i.e. before epoch numbers ) ending_word_boundary = ( From 7355768d54cb0168dd86bb86298a9a5a1bd6e351 Mon Sep 17 00:00:00 2001 From: Howard Willard Date: Tue, 26 Nov 2019 10:29:03 -0500 Subject: [PATCH 355/649] Multiple granularities logic simplification --- arrow/arrow.py | 25 ++++++++++++------------- docs/index.rst | 11 +++++++++++ setup.cfg | 2 +- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 2e5b219db..483757272 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -13,7 +13,6 @@ from datetime import tzinfo as dt_tzinfo from math import trunc -import six from dateutil import tz as dateutil_tz from dateutil.relativedelta import relativedelta @@ -58,11 +57,11 @@ class Arrow(object): _ATTRS_PLURAL = ["{}s".format(a) for a in _ATTRS] _MONTHS_PER_QUARTER = 3 _SECS_PER_MINUTE = float(60) - _SECS_PER_HOUR = float(3600) # 60 * 60 - _SECS_PER_DAY = float(86400) # 60 * 60 * 24 - _SECS_PER_WEEK = float(604800) # 60 * 60 * 24 * 7 - _SECS_PER_MONTH = float(2635200) # 60 * 60 * 24 * 30.5 - _SECS_PER_YEAR = float(31557600) # 60 * 60 * 24 * 365.25 + _SECS_PER_HOUR = float(60 * 60) + _SECS_PER_DAY = float(60 * 60 * 24) + _SECS_PER_WEEK = float(60 * 60 * 24 * 7) + _SECS_PER_MONTH = float(60 * 60 * 24 * 30.5) + _SECS_PER_YEAR = float(60 * 60 * 24 * 365.25) def __init__( self, year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None @@ -948,7 +947,7 @@ def humanize( years = sign * int(max(delta / 31536000, 2)) return locale.describe("years", years, only_distance=only_distance) - elif isinstance(granularity, six.string_types): + elif util.isstr(granularity): if granularity == "second": delta = sign * delta if abs(delta) < 2: @@ -978,27 +977,27 @@ def humanize( timeframes = [] if "year" in granularity: years = sign * delta / self._SECS_PER_YEAR - delta -= sign * trunc(years) * self._SECS_PER_YEAR + delta = delta % self._SECS_PER_YEAR timeframes.append(["year", years]) if "month" in granularity: months = sign * delta / self._SECS_PER_MONTH - delta -= sign * trunc(months) * self._SECS_PER_MONTH + delta = delta % self._SECS_PER_MONTH timeframes.append(["month", months]) if "week" in granularity: weeks = sign * delta / self._SECS_PER_WEEK - delta -= sign * trunc(weeks) * self._SECS_PER_WEEK + delta = delta % self._SECS_PER_WEEK timeframes.append(["week", weeks]) if "day" in granularity: days = sign * delta / self._SECS_PER_DAY - delta -= sign * trunc(days) * self._SECS_PER_DAY + delta = delta % self._SECS_PER_DAY timeframes.append(["day", days]) if "hour" in granularity: hours = sign * delta / self._SECS_PER_HOUR - delta -= sign * trunc(hours) * self._SECS_PER_HOUR + delta = delta % self._SECS_PER_HOUR timeframes.append(["hour", hours]) if "minute" in granularity: minutes = sign * delta / self._SECS_PER_MINUTE - delta -= sign * trunc(minutes) * self._SECS_PER_MINUTE + delta = delta % self._SECS_PER_MINUTE timeframes.append(["minute", minutes]) if "second" in granularity: seconds = sign * delta diff --git a/docs/index.rst b/docs/index.rst index 16ffb347d..af1e39ccd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -208,6 +208,17 @@ Or another Arrow, or datetime: >>> future.humanize(present) 'in 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' + Support for a growing number of locales (see ``locales.py`` for supported languages): .. code-block:: python diff --git a/setup.cfg b/setup.cfg index 2ab246006..08cae3ddf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ ignore = E203,E501,W503 line_length = 88 multi_line_output = 3 include_trailing_comma = true -known_third_party = chai,dateparser,dateutil,mock,pytz,setuptools,simplejson,six +known_third_party = chai,dateparser,dateutil,mock,pytz,setuptools,simplejson [bdist_wheel] universal = 1 From 9d23e479a216b49c5dbb8beb28905b97e13a1ec8 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Fri, 6 Dec 2019 11:34:57 +0000 Subject: [PATCH 356/649] Refactor logic statements --- arrow/factory.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/arrow/factory.py b/arrow/factory.py index c9310cd24..bf9e1f3ab 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -169,12 +169,11 @@ def get(self, *args, **kwargs): return self.type.utcnow() # try (int, float) -> from timestamp with tz - elif not isstr(arg) and is_timestamp(arg) and tz is not None: - return self.type.fromtimestamp(arg, tzinfo=tz) - - # try (int, float) -> utc, from timestamp. elif not isstr(arg) and is_timestamp(arg): - return self.type.utcfromtimestamp(arg) + if tz is None: + # set to UTC by default + tz = dateutil_tz.tzutc() + return self.type.fromtimestamp(arg, tzinfo=tz) # (Arrow) -> from the object's datetime. elif isinstance(arg, Arrow): From 88049db0f802187dc907f1b329f60d6b2442a5ef Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Fri, 6 Dec 2019 19:34:07 +0000 Subject: [PATCH 357/649] Test on float timestamp rather than integer --- tests/factory_tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/factory_tests.py b/tests/factory_tests.py index 8467fcc28..d2e1dab5a 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -75,13 +75,13 @@ def test_one_arg_timestamp(self): def test_one_arg_timestamp_with_tzinfo(self): - int_timestamp = int(time.time()) - timestamp_dt = datetime.fromtimestamp(int_timestamp, tz=tz.tzutc()).astimezone( + timestamp = time.time() + timestamp_dt = datetime.fromtimestamp(timestamp, tz=tz.tzutc()).astimezone( tz.gettz("US/Pacific") ) timezone = tz.gettz("US/Pacific") - assertDtEqual(self.factory.get(int_timestamp, tzinfo=timezone), timestamp_dt) + assertDtEqual(self.factory.get(timestamp, tzinfo=timezone), timestamp_dt) def test_one_arg_arrow(self): From a593b067235bdb981ed3706065ca19c500e25150 Mon Sep 17 00:00:00 2001 From: Howard Willard Date: Mon, 9 Dec 2019 13:04:46 -0500 Subject: [PATCH 358/649] Style fixes, update test, documentation additions --- arrow/arrow.py | 23 +++++++++++++++-------- docs/index.rst | 16 ++++++++++++++++ tests/arrow_tests.py | 17 ++++++++++++----- 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 483757272..cc9d04153 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -977,37 +977,44 @@ def humanize( timeframes = [] if "year" in granularity: years = sign * delta / self._SECS_PER_YEAR - delta = delta % self._SECS_PER_YEAR + delta %= self._SECS_PER_YEAR timeframes.append(["year", years]) + if "month" in granularity: months = sign * delta / self._SECS_PER_MONTH - delta = delta % self._SECS_PER_MONTH + delta %= self._SECS_PER_MONTH timeframes.append(["month", months]) + if "week" in granularity: weeks = sign * delta / self._SECS_PER_WEEK - delta = delta % self._SECS_PER_WEEK + delta %= self._SECS_PER_WEEK timeframes.append(["week", weeks]) + if "day" in granularity: days = sign * delta / self._SECS_PER_DAY - delta = delta % self._SECS_PER_DAY + delta %= self._SECS_PER_DAY timeframes.append(["day", days]) + if "hour" in granularity: hours = sign * delta / self._SECS_PER_HOUR - delta = delta % self._SECS_PER_HOUR + delta %= self._SECS_PER_HOUR timeframes.append(["hour", hours]) + if "minute" in granularity: minutes = sign * delta / self._SECS_PER_MINUTE - delta = delta % self._SECS_PER_MINUTE + delta %= self._SECS_PER_MINUTE timeframes.append(["minute", minutes]) + if "second" in granularity: seconds = sign * delta timeframes.append(["second", seconds]) + if len(timeframes) < len(granularity): raise AttributeError( "Invalid level of granularity. Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'" ) - for index in range(len(timeframes)): - gran, delta = timeframes[index] + + for index, (_, delta) in enumerate(timeframes): if trunc(abs(delta)) != 1: timeframes[index][0] += "s" return locale.describe_multi(timeframes, only_distance=only_distance) diff --git a/docs/index.rst b/docs/index.rst index af1e39ccd..d3371ebcb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -208,6 +208,18 @@ Or another Arrow, or datetime: >>> 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 @@ -218,6 +230,10 @@ Indicate a specific time granularity (or multiple): '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): diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 22587cb13..5ab01f4e5 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1531,15 +1531,22 @@ def test_multiple_granularity(self): later108onlydistance = self.now.shift(seconds=10 ** 8) self.assertEqual( - self.now.humanize(later108onlydistance, granularity=["year"]), "3 years ago" + self.now.humanize( + later108onlydistance, only_distance=True, granularity=["year"] + ), + "3 years", ) self.assertEqual( - self.now.humanize(later108onlydistance, granularity=["month", "week"]), - "37 months and 4 weeks ago", + self.now.humanize( + later108onlydistance, only_distance=True, granularity=["month", "week"] + ), + "37 months and 4 weeks", ) self.assertEqual( - self.now.humanize(later108onlydistance, granularity=["year", "second"]), - "3 years and seconds ago", + self.now.humanize( + later108onlydistance, only_distance=True, granularity=["year", "second"] + ), + "3 years and seconds", ) def test_seconds(self): From 545dead00a0e2c648158d59087dd8929be9e6fa7 Mon Sep 17 00:00:00 2001 From: Robin van Dijk Date: Fri, 20 Dec 2019 16:42:33 +0100 Subject: [PATCH 359/649] add week to the dutch/nl locale --- arrow/locales.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 9b1f6170e..34d4bda43 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1006,6 +1006,8 @@ class DutchLocale(Locale): "hours": "{0} uur", "day": "een dag", "days": "{0} dagen", + "week": "een week", + "weeks": "{0} weken", "month": "een maand", "months": "{0} maanden", "year": "een jaar", From 1cd7f94ef60e7718667fc0cce5ad5ed62db58b9a Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Wed, 1 Jan 2020 21:43:48 +0000 Subject: [PATCH 360/649] Revert support for negative timestamps on Windows (#745) --- arrow/arrow.py | 4 +-- arrow/parser.py | 6 ++--- arrow/util.py | 53 +----------------------------------- tests/parser_tests.py | 31 ++++++++++++--------- tests/util_tests.py | 63 ------------------------------------------- 5 files changed, 24 insertions(+), 133 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 28e0b4c5d..89c3e50ae 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -162,7 +162,7 @@ def fromtimestamp(cls, timestamp, tzinfo=None): "The provided timestamp '{}' is invalid.".format(timestamp) ) - dt = util.safe_fromtimestamp(float(timestamp), tzinfo) + dt = datetime.fromtimestamp(float(timestamp), tzinfo) return cls( dt.year, @@ -188,7 +188,7 @@ def utcfromtimestamp(cls, timestamp): "The provided timestamp '{}' is invalid.".format(timestamp) ) - dt = util.safe_utcfromtimestamp(float(timestamp)) + dt = datetime.utcfromtimestamp(float(timestamp)) return cls( dt.year, diff --git a/arrow/parser.py b/arrow/parser.py index 5f8c07b5e..379c68737 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -6,7 +6,7 @@ from dateutil import tz -from arrow import locales, util +from arrow import locales from arrow.constants import MAX_TIMESTAMP, MAX_TIMESTAMP_MS, MAX_TIMESTAMP_US try: @@ -383,7 +383,7 @@ def _build_datetime(parts): timestamp = parts.get("timestamp") if timestamp is not None: - return util.safe_fromtimestamp(timestamp, tz=tz.tzutc()) + return datetime.fromtimestamp(timestamp, tz=tz.tzutc()) expanded_timestamp = parts.get("expanded_timestamp") @@ -401,7 +401,7 @@ def _build_datetime(parts): ) ) - return util.safe_fromtimestamp(expanded_timestamp, tz=tz.tzutc()) + return datetime.fromtimestamp(expanded_timestamp, tz=tz.tzutc()) day_of_year = parts.get("day_of_year") diff --git a/arrow/util.py b/arrow/util.py index cf3bf5bba..62f1a0537 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -2,11 +2,6 @@ from __future__ import absolute_import import datetime -import math -import time -from os import name as os_name - -import dateutil def total_seconds(td): # pragma: no cover @@ -28,44 +23,6 @@ def is_timestamp(value): return False -def windows_datetime_from_timestamp(timestamp, tz=None): - """Computes datetime from timestamp. Supports negative timestamps on Windows platform.""" - sec_frac, sec = math.modf(timestamp) - dt = datetime.datetime(1970, 1, 1, tzinfo=dateutil.tz.tzutc()) + datetime.timedelta( - seconds=sec, microseconds=sec_frac * 1000000 - ) - if tz is None: - tz = dateutil.tz.tzlocal() - - if tz == dateutil.tz.tzlocal(): - # because datetime.astimezone does not work on Windows for tzlocal() and dates before the 1970-01-01 - # take timestamp from appropriate time of the year, because of daylight saving time changes - ts = time.mktime(dt.replace(year=1970).timetuple()) - dt += datetime.datetime.fromtimestamp(ts) - datetime.datetime.utcfromtimestamp( - ts - ) - dt = dt.replace(tzinfo=dateutil.tz.tzlocal()) - else: - dt = dt.astimezone(tz) - return dt - - -def safe_utcfromtimestamp(timestamp): - """ datetime.utcfromtimestamp alternative which supports negative timestamps on Windows platform.""" - if os_name == "nt" and timestamp < 0: - return windows_datetime_from_timestamp(timestamp, dateutil.tz.tzutc()) - else: - return datetime.datetime.utcfromtimestamp(timestamp) - - -def safe_fromtimestamp(timestamp, tz=None): - """ datetime.fromtimestamp alternative which supports negative timestamps on Windows platform.""" - if os_name == "nt" and timestamp < 0: - return windows_datetime_from_timestamp(timestamp, tz) - else: - return datetime.datetime.fromtimestamp(timestamp, tz) - - # Credit to https://stackoverflow.com/a/1700069 def iso_to_gregorian(iso_year, iso_week, iso_day): """Converts an ISO week date tuple into a datetime object.""" @@ -100,12 +57,4 @@ def isstr(s): return isinstance(s, str) -__all__ = [ - "total_seconds", - "is_timestamp", - "isstr", - "iso_to_gregorian", - "windows_datetime_from_timestamp", - "safe_utcfromtimestamp", - "safe_fromtimestamp", -] +__all__ = ["total_seconds", "is_timestamp", "isstr", "iso_to_gregorian"] diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 08c20d1ac..c72da044b 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -2,13 +2,14 @@ from __future__ import unicode_literals import calendar +import os import time from datetime import datetime from chai import Chai from dateutil import tz -from arrow import parser, util +from arrow import parser from arrow.constants import MAX_TIMESTAMP_US from arrow.parser import DateTimeParser, ParserError, ParserMatchError @@ -227,19 +228,23 @@ def test_parse_timestamp(self): self.parser.parse("{:f}123456".format(float_timestamp), "X"), self.expected ) - # regression test for issue #662 - negative_int_timestamp = -int_timestamp - self.expected = util.safe_fromtimestamp(negative_int_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:d}".format(negative_int_timestamp), "X"), self.expected - ) + # NOTE: negative timestamps cannot be handled by datetime on Window + # Must use timedelta to handle them. ref: https://stackoverflow.com/questions/36179914 + if os.name != "nt": + # regression test for issue #662 + negative_int_timestamp = -int_timestamp + self.expected = datetime.fromtimestamp(negative_int_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:d}".format(negative_int_timestamp), "X"), + self.expected, + ) - negative_float_timestamp = -float_timestamp - self.expected = util.safe_fromtimestamp(negative_float_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:f}".format(negative_float_timestamp), "X"), - self.expected, - ) + negative_float_timestamp = -float_timestamp + self.expected = datetime.fromtimestamp(negative_float_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:f}".format(negative_float_timestamp), "X"), + self.expected, + ) # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will # break cases like "15 Jul 2000" and a format list (see issue #447) diff --git a/tests/util_tests.py b/tests/util_tests.py index 980a0a93e..bb84c44b7 100644 --- a/tests/util_tests.py +++ b/tests/util_tests.py @@ -1,10 +1,7 @@ # -*- coding: utf-8 -*- import time -from datetime import datetime from chai import Chai -from dateutil import tz -from mock import patch from arrow import util @@ -36,63 +33,3 @@ def test_iso_gregorian(self): with self.assertRaises(ValueError): util.iso_to_gregorian(2013, 8, 0) - - def test_windows_datetime_from_timestamp(self): - timestamp = 1572204340.6460679 - result = util.windows_datetime_from_timestamp(timestamp) - expected = datetime.fromtimestamp(timestamp).replace(tzinfo=tz.tzlocal()) - self.assertEqual(result, expected) - - def test_windows_datetime_from_timestamp_utc(self): - timestamp = 1572204340.6460679 - result = util.windows_datetime_from_timestamp(timestamp, tz.tzutc()) - expected = datetime.utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) - self.assertEqual(result, expected) - - def test_safe_utcfromtimestamp(self): - timestamp = 1572204340.6460679 - result = util.safe_utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) - expected = datetime.utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) - self.assertEqual(result, expected) - - def test_safe_fromtimestamp_default_tz(self): - timestamp = 1572204340.6460679 - result = util.safe_fromtimestamp(timestamp).replace(tzinfo=tz.tzlocal()) - expected = datetime.fromtimestamp(timestamp).replace(tzinfo=tz.tzlocal()) - self.assertEqual(result, expected) - - def test_safe_fromtimestamp_paris_tz(self): - timestamp = 1572204340.6460679 - result = util.safe_fromtimestamp(timestamp, tz.gettz("Europe/Paris")) - expected = datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")) - self.assertEqual(result, expected) - - def test_safe_utcfromtimestamp_negative(self): - timestamp = -1572204340.6460679 - result = util.safe_utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) - expected = datetime(1920, 3, 7, 4, 34, 19, 353932, tzinfo=tz.tzutc()) - self.assertEqual(result, expected) - - def test_safe_fromtimestamp_negative(self): - timestamp = -1572204340.6460679 - result = util.safe_fromtimestamp(timestamp, tz.gettz("Europe/Paris")) - expected = datetime( - 1920, 3, 7, 5, 34, 19, 353932, tzinfo=tz.gettz("Europe/Paris") - ) - self.assertEqual(result, expected) - - @patch.object(util, "os_name", "nt") - def test_safe_utcfromtimestamp_negative_nt(self): - timestamp = -1572204340.6460679 - result = util.safe_utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc()) - expected = datetime(1920, 3, 7, 4, 34, 19, 353932, tzinfo=tz.tzutc()) - self.assertEqual(result, expected) - - @patch.object(util, "os_name", "nt") - def test_safe_fromtimestamp_negative_nt(self): - timestamp = -1572204340.6460679 - result = util.safe_fromtimestamp(timestamp, tz.gettz("Europe/Paris")) - expected = datetime( - 1920, 3, 7, 5, 34, 19, 353932, tzinfo=tz.gettz("Europe/Paris") - ) - self.assertEqual(result, expected) From 6a70b9d11372865d8e9dc67f20ca5f165adf0719 Mon Sep 17 00:00:00 2001 From: sunnz Date: Thu, 2 Jan 2020 08:53:37 +1100 Subject: [PATCH 361/649] Added Hong Kong (zh_hk) locale (#742) --- arrow/locales.py | 60 ++++++++++++++++++++++++++++++++++++++++++ tests/locales_tests.py | 24 +++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 34d4bda43..2c12cd964 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -932,6 +932,66 @@ class ChineseTWLocale(Locale): day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] +class HongKongLocale(Locale): + + names = ["zh_hk"] + + past = "{0}前" + future = "{0}後" + + timeframes = { + "now": "剛才", + "second": "1秒", + "seconds": "{0}秒", + "minute": "1分鐘", + "minutes": "{0}分鐘", + "hour": "1小時", + "hours": "{0}小時", + "day": "1天", + "days": "{0}天", + "week": "1星期", + "weeks": "{0}星期", + "month": "1個月", + "months": "{0}個月", + "year": "1年", + "years": "{0}年", + } + + month_names = [ + "", + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月", + ] + month_abbreviations = [ + "", + " 1", + " 2", + " 3", + " 4", + " 5", + " 6", + " 7", + " 8", + " 9", + "10", + "11", + "12", + ] + + day_names = ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] + day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] + + class KoreanLocale(Locale): names = ["ko", "ko_kr"] diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 86f6f5f78..83e56645a 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -928,3 +928,27 @@ def test_format_timeframe(self): self.assertEqual(self.locale._format_timeframe("months", 11), "11 meses") self.assertEqual(self.locale._format_timeframe("year", 1), "um ano") self.assertEqual(self.locale._format_timeframe("years", 12), "12 anos") + + +class HongKongLocaleTests(Chai): + def setUp(self): + super(HongKongLocaleTests, self).setUp() + + self.locale = locales.HongKongLocale() + + def test_format_timeframe(self): + self.assertEqual(self.locale._format_timeframe("now", 0), "剛才") + self.assertEqual(self.locale._format_timeframe("second", 1), "1秒") + self.assertEqual(self.locale._format_timeframe("seconds", 30), "30秒") + self.assertEqual(self.locale._format_timeframe("minute", 1), "1分鐘") + self.assertEqual(self.locale._format_timeframe("minutes", 40), "40分鐘") + self.assertEqual(self.locale._format_timeframe("hour", 1), "1小時") + self.assertEqual(self.locale._format_timeframe("hours", 23), "23小時") + self.assertEqual(self.locale._format_timeframe("day", 1), "1天") + self.assertEqual(self.locale._format_timeframe("days", 12), "12天") + self.assertEqual(self.locale._format_timeframe("week", 1), "1星期") + self.assertEqual(self.locale._format_timeframe("weeks", 38), "38星期") + self.assertEqual(self.locale._format_timeframe("month", 1), "1個月") + self.assertEqual(self.locale._format_timeframe("months", 11), "11個月") + self.assertEqual(self.locale._format_timeframe("year", 1), "1年") + self.assertEqual(self.locale._format_timeframe("years", 12), "12年") From b1b37cee7a9a870c666728edfb8bafa689a11b54 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Wed, 1 Jan 2020 17:24:55 -0500 Subject: [PATCH 362/649] Fixed arrow.get with a timestamp and a timezone string (#746) * Fixed arrow.get with a timestamp and a timezone string and added comments. * Removed pragma from util.py * Updated Travis Python versions --- .travis.yml | 4 ++-- arrow/arrow.py | 24 +++++++++--------------- arrow/util.py | 3 ++- tests/arrow_tests.py | 6 ++++++ tests/util_tests.py | 5 +++++ 5 files changed, 24 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index b86f51bca..e549ef4d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,13 +24,13 @@ matrix: os: windows language: shell # 'language: python' is an error on Travis CI Windows before_install: - - choco install python --version 3.7.4 + - choco install python --version 3.7.5 - python -m pip install --upgrade pip env: - PATH=/c/Python37:/c/Python37/Scripts:$PATH - TOXENV=py37 - name: "Python 3.8" - python: "3.8-dev" + python: "3.8" env: TOXENV=py38 - name: "Linting" python: "3.7" diff --git a/arrow/arrow.py b/arrow/arrow.py index 89c3e50ae..1099a6dec 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -146,16 +146,12 @@ def fromtimestamp(cls, timestamp, tzinfo=None): :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. - - Timestamps should always be UTC. If you have a non-UTC timestamp:: - - >>> arrow.Arrow.utcfromtimestamp(1367900664).replace(tzinfo='US/Pacific') - - """ if tzinfo is None: tzinfo = dateutil_tz.tzlocal() + elif util.isstr(tzinfo): + tzinfo = parser.TzinfoParser.parse(tzinfo) if not util.is_timestamp(timestamp): raise ValueError( @@ -1423,19 +1419,17 @@ def _get_tzinfo(tz_expr): @classmethod def _get_datetime(cls, expr): - + """Get datetime object for a specified expression.""" if isinstance(expr, Arrow): return expr.datetime - - if isinstance(expr, datetime): + elif isinstance(expr, datetime): return expr - - try: - expr = float(expr) - return cls.utcfromtimestamp(expr).datetime - except Exception: + elif util.is_timestamp(expr): + timestamp = float(expr) + return cls.utcfromtimestamp(timestamp).datetime + else: raise ValueError( - "'{}' not recognized as a timestamp or datetime".format(expr) + "'{}' not recognized as a datetime or timestamp.".format(expr) ) @classmethod diff --git a/arrow/util.py b/arrow/util.py index 62f1a0537..7037f1f7d 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -4,7 +4,8 @@ import datetime -def total_seconds(td): # pragma: no cover +def total_seconds(td): + """Get total seconds for timedelta.""" return td.total_seconds() diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 061ed2002..c35832937 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -103,6 +103,12 @@ def test_fromtimestamp(self): datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), ) + result = arrow.Arrow.fromtimestamp(timestamp, tzinfo="Europe/Paris") + assertDtEqual( + result._datetime, + datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), + ) + with self.assertRaises(ValueError): arrow.Arrow.fromtimestamp("invalid timestamp") diff --git a/tests/util_tests.py b/tests/util_tests.py index bb84c44b7..804ae62a2 100644 --- a/tests/util_tests.py +++ b/tests/util_tests.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import time +from datetime import datetime from chai import Chai @@ -7,6 +8,10 @@ class UtilTests(Chai): + def test_total_seconds(self): + td = datetime(2019, 1, 1) - datetime(2018, 1, 1) + self.assertEqual(util.total_seconds(td), td.total_seconds()) + def test_is_timestamp(self): timestamp_float = time.time() timestamp_int = int(timestamp_float) From b12f4a6d23ef8691f1485651358270374920fdc7 Mon Sep 17 00:00:00 2001 From: JunhwanK <35044762+JunhwanK@users.noreply.github.com> Date: Wed, 1 Jan 2020 18:37:37 -0500 Subject: [PATCH 363/649] allowing plural 'name' arguments to _get_frames (#735) * added tests for allowing plural frames into _get_frames * implemented parsing plural arguments in _get_frames * Fixed arrow.get with a timestamp and a timezone string and added comments. * Remove unnecessary additional attribute in shift function. Co-authored-by: Jad Chaar --- arrow/arrow.py | 17 +++++++++++++++-- tests/arrow_tests.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 1099a6dec..aff35086e 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1437,13 +1437,26 @@ def _get_frames(cls, name): if name in cls._ATTRS: return name, "{}s".format(name), 1 - + elif name[-1] == "s" and name[:-1] in cls._ATTRS: + return name[:-1], name, 1 elif name in ["week", "weeks"]: return "week", "weeks", 1 elif name in ["quarter", "quarters"]: return "quarter", "months", 3 - supported = ", ".join(cls._ATTRS + ["week", "weeks"] + ["quarter", "quarters"]) + supported = ", ".join( + [ + "year(s)", + "month(s)", + "day(s)", + "hour(s)", + "minute(s)", + "second(s)", + "microsecond(s)", + "week(s)", + "quarter(s)", + ] + ) raise AttributeError( "range/span over frame {} not supported. Supported frames: {}".format( name, supported diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index c35832937..6214ada92 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1058,6 +1058,36 @@ 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) + ) + ) + + self.assertEqual( + result, + [ + ( + arrow.Arrow(2013, 1, 1, 0), + arrow.Arrow(2013, 1, 1, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 2, 0), + arrow.Arrow(2013, 1, 2, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 3, 0), + arrow.Arrow(2013, 1, 3, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 4, 0), + arrow.Arrow(2013, 1, 4, 23, 59, 59, 999999), + ), + ], + ) + def test_hour(self): result = list( From 379e08f33391d6651a5803719a584b11da633309 Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Thu, 2 Jan 2020 22:27:51 +0000 Subject: [PATCH 364/649] Add Arrow DST tests based on two false positives (#747) * added Arrow DST tests based on two false positive open issues * tests for dst * Add comments explaining the new tests * Move comments to docstring, rename test class Co-authored-by: Mark Fonte --- docs/index.rst | 2 +- tests/arrow_tests.py | 52 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 2122bce47..c310f0572 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -163,7 +163,7 @@ Format Convert ~~~~~~~ -Convert to timezones by name or tzinfo: +Convert from UTC to other timezones by name or tzinfo: .. code-block:: python diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 6214ada92..c92f78a80 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -512,6 +512,58 @@ def test_strftime(self): self.assertEqual(result, self.arrow._datetime.strftime("%Y")) +class ArrowFalsePositiveDstTests(Chai): + """These tests relate to issues #376 and #551. + The key points in both issues are that arrow will assign a UTC timezone if none is provided and + .to() will change other attributes to be correct whereas .replace() only changes the specified attribute. + + Issue 376 + >>> arrow.get('2016-11-06').to('America/New_York').ceil('day') + < Arrow [2016-11-05T23:59:59.999999-04:00] > + + Issue 551 + >>> just_before = arrow.get('2018-11-04T01:59:59.999999') + >>> just_before + 2018-11-04T01:59:59.999999+00:00 + >>> just_after = just_before.shift(microseconds=1) + >>> just_after + 2018-11-04T02:00:00+00:00 + >>> just_before_eastern = just_before.replace(tzinfo='US/Eastern') + >>> just_before_eastern + 2018-11-04T01:59:59.999999-04:00 + >>> just_after_eastern = just_after.replace(tzinfo='US/Eastern') + >>> just_after_eastern + 2018-11-04T02:00:00-05:00 + """ + + def setUp(self): + + super(ArrowFalsePositiveDstTests, self).setUp() + self.before_1 = arrow.Arrow( + 2016, 11, 6, 3, 59, tzinfo=tz.gettz("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.after_2 = arrow.Arrow( + 2016, 11, 6, 23, 59, tzinfo=tz.gettz("America/New_York") + ) + self.before_3 = arrow.Arrow( + 2018, 11, 4, 3, 59, tzinfo=tz.gettz("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.after_4 = arrow.Arrow( + 2018, 11, 4, 23, 59, tzinfo=tz.gettz("America/New_York") + ) + + def test_dst(self): + + self.assertEqual(self.before_1.day, self.before_2.day) + self.assertEqual(self.after_1.day, self.after_2.day) + self.assertEqual(self.before_3.day, self.before_4.day) + self.assertEqual(self.after_3.day, self.after_4.day) + + class ArrowConversionTests(Chai): def test_to(self): From 6adb8dbccc39e38813136970950700276c680618 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Fri, 3 Jan 2020 11:05:06 -0500 Subject: [PATCH 365/649] Correct error in humanize() granularities for single seconds (#748) * Fixed arrow.get with a timestamp and a timezone string and added comments. * Fixed a couple of bugs with humanize * Small syntax tweaks * Added some test cases and filled in bare exception message. * Cleaned up comment on loop. --- arrow/arrow.py | 23 +++++-- arrow/locales.py | 141 +++++++++++++++++++++++++++-------------- tests/arrow_tests.py | 48 +++++++++++--- tests/locales_tests.py | 14 ++-- 4 files changed, 157 insertions(+), 69 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index aff35086e..1bb219236 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -904,7 +904,12 @@ def humanize( dt = other.astimezone(self._datetime.tzinfo) else: - raise TypeError() + raise TypeError( + "Invalid 'other' argument of type '{}'. " + "Argument must be of type None, Arrow, or datetime.".format( + type(other).__name__ + ) + ) if isinstance(granularity, list) and len(granularity) == 1: granularity = granularity[0] @@ -939,7 +944,8 @@ def humanize( hours = sign * int(max(delta / 3600, 2)) return locale.describe("hours", hours, only_distance=only_distance) - elif diff < 129600: + # anything less than 48 hours should be 1 day + elif diff < 172800: return locale.describe("day", sign, only_distance=only_distance) elif diff < 554400: days = sign * int(max(delta / 86400, 2)) @@ -1033,17 +1039,20 @@ def humanize( if len(timeframes) < len(granularity): raise AttributeError( - "Invalid level of granularity. Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'" + "Invalid level of granularity. " + "Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'." ) - for index, (_, delta) in enumerate(timeframes): - if trunc(abs(delta)) != 1: - timeframes[index][0] += "s" + for tf in timeframes: + # Make granularity plural if the delta is not equal to 1 + if trunc(abs(tf[1])) != 1: + tf[0] += "s" return locale.describe_multi(timeframes, only_distance=only_distance) except KeyError as e: raise ValueError( - "Humanization of the {} granularity is not currently translated in the '{}' locale. Please consider making a contribution to this locale.".format( + "Humanization of the {} granularity is not currently translated in the '{}' locale. " + "Please consider making a contribution to this locale.".format( e, locale_name ) ) diff --git a/arrow/locales.py b/arrow/locales.py index 2c12cd964..a42947f66 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -32,6 +32,7 @@ class Locale(object): timeframes = { "now": "", + "second": "", "seconds": "", "minute": "", "minutes": "", @@ -83,7 +84,7 @@ def describe_multi(self, timeframes, only_distance=False): """ Describes a delta within multiple timeframes in plain language. :param timeframes: a list of string, quantity pairs each representing a timeframe and delta. - :param only_distance: return only distance eg: "2 hours 11 seconds" without "in" or "ago" keywords + :param only_distance: return only distance eg: "2 hours and 11 seconds" without "in" or "ago" keywords """ humanized = "" @@ -225,7 +226,8 @@ class EnglishLocale(Locale): timeframes = { "now": "just now", - "seconds": "seconds", + "second": "a second", + "seconds": "{0} seconds", "minute": "a minute", "minutes": "{0} minutes", "hour": "an hour", @@ -321,7 +323,8 @@ class ItalianLocale(Locale): timeframes = { "now": "adesso", - "seconds": "qualche secondo", + "second": "un secondo", + "seconds": "{0} qualche secondo", "minute": "un minuto", "minutes": "{0} minuti", "hour": "un'ora", @@ -391,7 +394,8 @@ class SpanishLocale(Locale): timeframes = { "now": "ahora", - "seconds": "segundos", + "second": "un segundo", + "seconds": "{0} segundos", "minute": "un minuto", "minutes": "{0} minutos", "hour": "una hora", @@ -465,7 +469,8 @@ class FrenchLocale(Locale): timeframes = { "now": "maintenant", - "seconds": "quelques secondes", + "second": "une seconde", + "seconds": "{0} quelques secondes", "minute": "une minute", "minutes": "{0} minutes", "hour": "une heure", @@ -543,7 +548,8 @@ class GreekLocale(Locale): timeframes = { "now": "τώρα", - "seconds": "δευτερόλεπτα", + "second": "ένα δεύτερο", + "seconds": "{0} δευτερόλεπτα", "minute": "ένα λεπτό", "minutes": "{0} λεπτά", "hour": "μία ώρα", @@ -609,7 +615,8 @@ class JapaneseLocale(Locale): timeframes = { "now": "現在", - "seconds": "数秒", + "second": "二番目の", + "seconds": "{0}数秒", "minute": "1分", "minutes": "{0}分", "hour": "1時間", @@ -669,7 +676,8 @@ class SwedishLocale(Locale): timeframes = { "now": "just nu", - "seconds": "några sekunder", + "second": "en sekund", + "seconds": "{0} några sekunder", "minute": "en minut", "minutes": "{0} minuter", "hour": "en timme", @@ -738,7 +746,8 @@ class FinnishLocale(Locale): timeframes = { "now": ["juuri nyt", "juuri nyt"], - "seconds": ["muutama sekunti", "muutaman sekunnin"], + "second": ["sekunti", "sekunti"], + "seconds": ["{0} muutama sekunti", "{0} muutaman sekunnin"], "minute": ["minuutti", "minuutin"], "minutes": ["{0} minuuttia", "{0} minuutin"], "hour": ["tunti", "tunnin"], @@ -825,7 +834,8 @@ class ChineseCNLocale(Locale): timeframes = { "now": "刚才", - "seconds": "几秒", + "second": "一秒", + "seconds": "{0}几秒", "minute": "1分钟", "minutes": "{0}分钟", "hour": "1小时", @@ -884,7 +894,8 @@ class ChineseTWLocale(Locale): timeframes = { "now": "剛才", - "seconds": "幾秒", + "second": "一秒", + "seconds": "{0}幾秒", "minute": "1分鐘", "minutes": "{0}分鐘", "hour": "1小時", @@ -1001,7 +1012,8 @@ class KoreanLocale(Locale): timeframes = { "now": "지금", - "seconds": "몇 초", + "second": "두 번째", + "seconds": "{0}몇 초", "minute": "1분", "minutes": "{0}분", "hour": "1시간", @@ -1059,7 +1071,8 @@ class DutchLocale(Locale): timeframes = { "now": "nu", - "seconds": "seconden", + "second": "een seconde", + "seconds": "{0} seconden", "minute": "een minuut", "minutes": "{0} minuten", "hour": "een uur", @@ -1147,7 +1160,8 @@ class BelarusianLocale(SlavicBaseLocale): timeframes = { "now": "зараз", - "seconds": "некалькі секунд", + "second": "секунду", + "seconds": "{0} некалькі секунд", "minute": "хвіліну", "minutes": ["{0} хвіліну", "{0} хвіліны", "{0} хвілін"], "hour": "гадзіну", @@ -1213,7 +1227,8 @@ class PolishLocale(SlavicBaseLocale): timeframes = { "now": "teraz", - "seconds": "kilka sekund", + "second": "sekunda", + "seconds": "{0} kilka sekund", "minute": "minutę", "minutes": ["{0} minut", "{0} minuty", "{0} minut"], "hour": "godzina", @@ -1279,7 +1294,8 @@ class RussianLocale(SlavicBaseLocale): timeframes = { "now": "сейчас", - "seconds": "несколько секунд", + "second": "Второй", + "seconds": "{0} несколько секунд", "minute": "минуту", "minutes": ["{0} минуту", "{0} минуты", "{0} минут"], "hour": "час", @@ -1347,7 +1363,8 @@ class AfrikaansLocale(Locale): timeframes = { "now": "nou", - "seconds": "sekondes", + "second": "n sekonde", + "seconds": "{0} sekondes", "minute": "minuut", "minutes": "{0} minute", "hour": "uur", @@ -1413,7 +1430,8 @@ class BulgarianLocale(SlavicBaseLocale): timeframes = { "now": "сега", - "seconds": "няколко секунди", + "second": "секунда", + "seconds": "{0} няколко секунди", "minute": "минута", "minutes": ["{0} минута", "{0} минути", "{0} минути"], "hour": "час", @@ -1479,7 +1497,8 @@ class UkrainianLocale(SlavicBaseLocale): timeframes = { "now": "зараз", - "seconds": "кілька секунд", + "second": "секунда", + "seconds": "{0} кілька секунд", "minute": "хвилину", "minutes": ["{0} хвилину", "{0} хвилини", "{0} хвилин"], "hour": "годину", @@ -1544,7 +1563,8 @@ class MacedonianLocale(SlavicBaseLocale): timeframes = { "now": "сега", - "seconds": "секунди", + "second": "секунда", + "seconds": "{0} секунди", "minute": "една минута", "minutes": ["{0} минута", "{0} минути", "{0} минути"], "hour": "еден саат", @@ -1620,7 +1640,8 @@ class DeutschBaseLocale(Locale): timeframes = { "now": "gerade eben", - "seconds": "Sekunden", + "second": "eine Sekunde", + "seconds": "{0} Sekunden", "minute": "einer Minute", "minutes": "{0} Minuten", "hour": "einer Stunde", @@ -1740,7 +1761,8 @@ class NorwegianLocale(Locale): timeframes = { "now": "nå nettopp", - "seconds": "noen sekunder", + "second": "et sekund", + "seconds": "{0} noen sekunder", "minute": "ett minutt", "minutes": "{0} minutter", "hour": "en time", @@ -1806,7 +1828,8 @@ class NewNorwegianLocale(Locale): timeframes = { "now": "no nettopp", - "seconds": "nokre sekund", + "second": "et sekund", + "seconds": "{0} nokre sekund", "minute": "ett minutt", "minutes": "{0} minutt", "hour": "ein time", @@ -2010,7 +2033,8 @@ class TagalogLocale(Locale): timeframes = { "now": "ngayon lang", - "seconds": "segundo", + "second": "isang segundo", + "seconds": "{0} segundo", "minute": "isang minuto", "minutes": "{0} minuto", "hour": "isang oras", @@ -2079,7 +2103,8 @@ class VietnameseLocale(Locale): timeframes = { "now": "hiện tại", - "seconds": "giây", + "second": "một giây", + "seconds": "{0} giây", "minute": "một phút", "minutes": "{0} phút", "hour": "một giờ", @@ -2147,7 +2172,8 @@ class TurkishLocale(Locale): timeframes = { "now": "şimdi", - "seconds": "saniye", + "second": "bir saniye", + "seconds": "{0} saniye", "minute": "bir dakika", "minutes": "{0} dakika", "hour": "bir saat", @@ -2213,7 +2239,8 @@ class AzerbaijaniLocale(Locale): timeframes = { "now": "indi", - "seconds": "saniyə", + "second": "saniyə", + "seconds": "{0} saniyə", "minute": "bir dəqiqə", "minutes": "{0} dəqiqə", "hour": "bir saat", @@ -2297,6 +2324,7 @@ class ArabicLocale(Locale): timeframes = { "now": "الآن", + "second": "ثانية", "seconds": {"double": "ثانيتين", "ten": "{0} ثوان", "higher": "{0} ثانية"}, "minute": "دقيقة", "minutes": {"double": "دقيقتين", "ten": "{0} دقائق", "higher": "{0} دقيقة"}, @@ -2521,7 +2549,8 @@ def _format_timeframe(self, timeframe, delta): timeframes = { "now": "rétt í þessu", - "seconds": ("nokkrum sekúndum", "nokkrar sekúndur"), + "second": ("sekúndu", "sekúndu"), + "seconds": ("{0} nokkrum sekúndum", "nokkrar sekúndur"), "minute": ("einni mínútu", "eina mínútu"), "minutes": ("{0} mínútum", "{0} mínútur"), "hour": ("einum tíma", "einn tíma"), @@ -2590,7 +2619,8 @@ class DanishLocale(Locale): timeframes = { "now": "lige nu", - "seconds": "et par sekunder", + "second": "et sekund", + "seconds": "{0} et par sekunder", "minute": "et minut", "minutes": "{0} minutter", "hour": "en time", @@ -2656,7 +2686,8 @@ class MalayalamLocale(Locale): timeframes = { "now": "ഇപ്പോൾ", - "seconds": "സെക്കന്റ്‌", + "second": "ഒരു നിമിഷം", + "seconds": "{0} സെക്കന്റ്‌", "minute": "ഒരു മിനിറ്റ്", "minutes": "{0} മിനിറ്റ്", "hour": "ഒരു മണിക്കൂർ", @@ -2729,7 +2760,8 @@ class HindiLocale(Locale): timeframes = { "now": "अभी", - "seconds": "सेकंड्", + "second": "एक पल", + "seconds": "{0} सेकंड्", "minute": "एक मिनट ", "minutes": "{0} मिनट ", "hour": "एक घंटा", @@ -2793,6 +2825,7 @@ class CzechLocale(Locale): timeframes = { "now": "Teď", + "second": {"past": "vteřina", "future": "vteřina", "zero": "vteřina"}, "seconds": {"past": "{0} sekundami", "future": ["{0} sekundy", "{0} sekund"]}, "minute": {"past": "minutou", "future": "minutu", "zero": "{0} minut"}, "minutes": {"past": "{0} minutami", "future": ["{0} minuty", "{0} minut"]}, @@ -2879,6 +2912,7 @@ class SlovakLocale(Locale): timeframes = { "now": "Teraz", + "second": {"past": "druhý", "future": "druhý", "zero": "druhý"}, "seconds": {"past": "pár sekundami", "future": ["{0} sekundy", "{0} sekúnd"]}, "minute": {"past": "minútou", "future": "minútu", "zero": "{0} minút"}, "minutes": {"past": "{0} minútami", "future": ["{0} minúty", "{0} minút"]}, @@ -2970,7 +3004,8 @@ class FarsiLocale(Locale): timeframes = { "now": "اکنون", - "seconds": "ثانیه", + "second": "یک لحظه", + "seconds": "{0} ثانیه", "minute": "یک دقیقه", "minutes": "{0} دقیقه", "hour": "یک ساعت", @@ -3043,7 +3078,8 @@ class HebrewLocale(Locale): timeframes = { "now": "הרגע", - "seconds": "שניות", + "second": "שנייה", + "seconds": "{0} שניות", "minute": "דקה", "minutes": "{0} דקות", "hour": "שעה", @@ -3119,7 +3155,8 @@ class MarathiLocale(Locale): timeframes = { "now": "सद्य", - "seconds": "सेकंद", + "second": "एक सेकंद", + "seconds": "{0} सेकंद", "minute": "एक मिनिट ", "minutes": "{0} मिनिट ", "hour": "एक तास", @@ -3198,7 +3235,8 @@ class CatalanLocale(Locale): timeframes = { "now": "Ara mateix", - "seconds": "segons", + "second": "un segon", + "seconds": "{0} segons", "minute": "1 minut", "minutes": "{0} minuts", "hour": "una hora", @@ -3270,7 +3308,8 @@ class BasqueLocale(Locale): timeframes = { "now": "Orain", - "seconds": "segundu", + "second": "segundo bat", + "seconds": "{0} segundu", "minute": "minutu bat", "minutes": "{0} minutu", "hour": "ordu bat", @@ -3335,7 +3374,8 @@ class HungarianLocale(Locale): timeframes = { "now": "éppen most", - "seconds": {"past": "másodpercekkel", "future": "pár másodperc"}, + "second": {"past": "egy második", "future": "egy második"}, + "seconds": {"past": "{0} másodpercekkel", "future": "{0} pár másodperc"}, "minute": {"past": "egy perccel", "future": "egy perc"}, "minutes": {"past": "{0} perccel", "future": "{0} perc"}, "hour": {"past": "egy órával", "future": "egy óra"}, @@ -3412,7 +3452,8 @@ class EsperantoLocale(Locale): timeframes = { "now": "nun", - "seconds": "kelkaj sekundoj", + "second": "sekundo", + "seconds": "{0} kelkaj sekundoj", "minute": "unu minuto", "minutes": "{0} minutoj", "hour": "un horo", @@ -3485,7 +3526,8 @@ class ThaiLocale(Locale): timeframes = { "now": "ขณะนี้", - "seconds": "ไม่กี่วินาที", + "second": "วินาที", + "seconds": "{0} ไม่กี่วินาที", "minute": "1 นาที", "minutes": "{0} นาที", "hour": "1 ชั่วโมง", @@ -3565,7 +3607,8 @@ class BengaliLocale(Locale): timeframes = { "now": "এখন", - "seconds": "সেকেন্ড", + "second": "একটি দ্বিতীয়", + "seconds": "{0} সেকেন্ড", "minute": "এক মিনিট", "minutes": "{0} মিনিট", "hour": "এক ঘণ্টা", @@ -3645,7 +3688,8 @@ class RomanshLocale(Locale): timeframes = { "now": "en quest mument", - "seconds": "secundas", + "second": "in secunda", + "seconds": "{0} secundas", "minute": "ina minuta", "minutes": "{0} minutas", "hour": "in'ura", @@ -3713,7 +3757,8 @@ class SwissLocale(Locale): timeframes = { "now": "gerade eben", - "seconds": "Sekunden", + "second": "eine Sekunde", + "seconds": "{0} Sekunden", "minute": "einer Minute", "minutes": "{0} Minuten", "hour": "einer Stunde", @@ -3783,7 +3828,8 @@ class RomanianLocale(Locale): timeframes = { "now": "acum", - "seconds": "câteva secunde", + "second": "o secunda", + "seconds": "{0} câteva secunde", "minute": "un minut", "minutes": "{0} minute", "hour": "o oră", @@ -3849,7 +3895,8 @@ class SlovenianLocale(Locale): timeframes = { "now": "zdaj", - "seconds": "sekund", + "second": "sekundo", + "seconds": "{0} sekund", "minute": "minuta", "minutes": "{0} minutami", "hour": "uro", @@ -3920,7 +3967,8 @@ class IndonesianLocale(Locale): timeframes = { "now": "baru saja", - "seconds": "detik", + "second": "1 sebentar", + "seconds": "{0} detik", "minute": "1 menit", "minutes": "{0} menit", "hour": "1 jam", @@ -3989,7 +4037,8 @@ class NepaliLocale(Locale): timeframes = { "now": "अहिले", - "seconds": "सेकण्ड", + "second": "एक सेकेन्ड", + "seconds": "{0} सेकण्ड", "minute": "मिनेट", "minutes": "{0} मिनेट", "hour": "एक घण्टा", diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index c92f78a80..9d6827660 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1544,10 +1544,10 @@ def test_granularity(self): later100 = self.now.shift(seconds=100) self.assertEqual( - self.now.humanize(later100, granularity="second"), "seconds ago" + self.now.humanize(later100, granularity="second"), "100 seconds ago" ) self.assertEqual( - later100.humanize(self.now, granularity="second"), "in seconds" + later100.humanize(self.now, granularity="second"), "in 100 seconds" ) self.assertEqual( self.now.humanize(later100, granularity="minute"), "a minute ago" @@ -1646,7 +1646,7 @@ def test_multiple_granularity(self): self.assertEqual(self.now.humanize(granularity=["second"]), "just now") self.assertEqual( self.now.humanize(granularity=["year", "month", "day", "hour", "second"]), - "in 0 years 0 months 0 days 0 hours and seconds", + "in 0 years 0 months 0 days 0 hours and 0 seconds", ) later4000 = self.now.shift(seconds=4000) @@ -1698,18 +1698,31 @@ def test_multiple_granularity(self): self.now.humanize( later108onlydistance, only_distance=True, granularity=["year", "second"] ), - "3 years and seconds", + "3 years and 5327200 seconds", + ) + + one_min_one_sec_ago = self.now.shift(minutes=-1, seconds=-1) + self.assertEqual( + one_min_one_sec_ago.humanize(self.now, granularity=["minute", "second"]), + "a minute and a second ago", + ) + + one_min_two_secs_ago = self.now.shift(minutes=-1, seconds=-2) + self.assertEqual( + one_min_two_secs_ago.humanize(self.now, granularity=["minute", "second"]), + "a minute and 2 seconds ago", ) def test_seconds(self): later = self.now.shift(seconds=10) - self.assertEqual(self.now.humanize(later), "seconds ago") - self.assertEqual(later.humanize(self.now), "in seconds") + # regression test for issue #727 + self.assertEqual(self.now.humanize(later), "10 seconds ago") + self.assertEqual(later.humanize(self.now), "in 10 seconds") - self.assertEqual(self.now.humanize(later, only_distance=True), "seconds") - self.assertEqual(later.humanize(self.now, only_distance=True), "seconds") + self.assertEqual(self.now.humanize(later, only_distance=True), "10 seconds") + self.assertEqual(later.humanize(self.now, only_distance=True), "10 seconds") def test_minute(self): @@ -1758,6 +1771,23 @@ def test_day(self): self.assertEqual(self.now.humanize(later), "a day ago") self.assertEqual(later.humanize(self.now), "in a day") + # regression test for issue #697 + less_than_48_hours = self.now.shift( + days=1, hours=23, seconds=59, microseconds=999999 + ) + self.assertEqual(self.now.humanize(less_than_48_hours), "a day ago") + self.assertEqual(less_than_48_hours.humanize(self.now), "in a day") + + less_than_48_hours_date = less_than_48_hours._datetime.date() + with self.assertRaises(TypeError): + # humanize other argument does not take raw datetime.date objects + self.now.humanize(less_than_48_hours_date) + + # convert from date to arrow object + less_than_48_hours_date = arrow.Arrow.fromdate(less_than_48_hours_date) + self.assertEqual(self.now.humanize(less_than_48_hours_date), "a day ago") + self.assertEqual(less_than_48_hours_date.humanize(self.now), "in a day") + self.assertEqual(self.now.humanize(later, only_distance=True), "a day") self.assertEqual(later.humanize(self.now, only_distance=True), "a day") @@ -1922,7 +1952,7 @@ def test_seconds(self): result = arw.humanize(self.datetime, locale="ru") - self.assertEqual(result, "через несколько секунд") + self.assertEqual(result, "через 44 несколько секунд") def test_years(self): diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 83e56645a..af205b435 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -153,9 +153,9 @@ def test_ordinal_number(self): def test_format_timeframe(self): locale = locales.SpanishLocale() self.assertEqual(locale._format_timeframe("now", 0), "ahora") - self.assertEqual(locale._format_timeframe("seconds", 1), "segundos") - self.assertEqual(locale._format_timeframe("seconds", 3), "segundos") - self.assertEqual(locale._format_timeframe("seconds", 30), "segundos") + self.assertEqual(locale._format_timeframe("seconds", 1), "1 segundos") + self.assertEqual(locale._format_timeframe("seconds", 3), "3 segundos") + self.assertEqual(locale._format_timeframe("seconds", 30), "30 segundos") self.assertEqual(locale._format_timeframe("minute", 1), "un minuto") self.assertEqual(locale._format_timeframe("minutes", 4), "4 minutos") self.assertEqual(locale._format_timeframe("minutes", 40), "40 minutos") @@ -176,9 +176,9 @@ def test_format_timeframe(self): self.assertEqual(locale._format_timeframe("years", 12), "12 años") self.assertEqual(locale._format_timeframe("now", 0), "ahora") - self.assertEqual(locale._format_timeframe("seconds", -1), "segundos") - self.assertEqual(locale._format_timeframe("seconds", -9), "segundos") - self.assertEqual(locale._format_timeframe("seconds", -12), "segundos") + self.assertEqual(locale._format_timeframe("seconds", -1), "1 segundos") + self.assertEqual(locale._format_timeframe("seconds", -9), "9 segundos") + self.assertEqual(locale._format_timeframe("seconds", -12), "12 segundos") self.assertEqual(locale._format_timeframe("minute", -1), "un minuto") self.assertEqual(locale._format_timeframe("minutes", -2), "2 minutos") self.assertEqual(locale._format_timeframe("minutes", -10), "10 minutos") @@ -805,7 +805,7 @@ def test_format_timeframe(self): self.assertEqual(self.locale._format_timeframe("month", 1), "isang buwan") self.assertEqual(self.locale._format_timeframe("year", 1), "isang taon") - self.assertEqual(self.locale._format_timeframe("seconds", 2), "segundo") + self.assertEqual(self.locale._format_timeframe("seconds", 2), "2 segundo") self.assertEqual(self.locale._format_timeframe("minutes", 3), "3 minuto") self.assertEqual(self.locale._format_timeframe("hours", 4), "4 oras") self.assertEqual(self.locale._format_timeframe("months", 5), "5 buwan") From c856ab9dad4901551f2bf7012f6a9dc2779091db Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Fri, 3 Jan 2020 16:27:27 +0000 Subject: [PATCH 366/649] Bump version to 0.15.5 and update CHANGELOG (#750) * Bump version to 0.15.5 and update CHANGELOG * Small tweaks to CHANGELOG --- CHANGELOG.rst | 29 +++++++++++++++++++++++++++++ arrow/_version.py | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2705c89d9..604bcd484 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,35 @@ Changelog ========= +0.15.5 (2020-01-03) +------------------- + +- [WARN] Python 2 reached EOL on 2020-01-01. arrow will **drop support** for Python 2 in a future release to be decided (see `#739 `_). +- [NEW] Added bounds parameter to ``span_range``, ``interval`` and ``span`` methods. This allows you to include or exclude the start and end values. +- [NEW] ``arrow.get()`` can now create arrow objects from a timestamp with a timezone, for example: + +.. code-block:: python + + >>> arrow.get(1367900664, tzinfo=tz.gettz('US/Pacific')) + + +- [NEW] ``humanize`` can now combine multiple levels of granularity, for example: + +.. code-block:: python + + >>> later140 = arrow.utcnow().shift(seconds=+8400) + >>> later140.humanize(granularity="minute") + 'in 139 minutes' + >>> later140.humanize(granularity=["hour", "minute"]) + 'in 2 hours and 19 minutes' + +- [NEW] Added Hong Kong locale (``zh_hk``). +- [NEW] Added ``humanize`` week granularity translation for Dutch. +- [NEW] Numbers are now displayed when using the seconds granularity in ``humanize``. +- [CHANGE] ``range`` now supports both the singular and plural forms of the ``frames`` argument (e.g. day and days). +- [FIX] Improved parsing of strings that contain punctuation. +- [FIX] Improved behaviour of ``humanize`` when singular seconds are involved. + 0.15.4 (2019-11-02) ------------------- diff --git a/arrow/_version.py b/arrow/_version.py index 8782a8b49..32e2f393c 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.15.4" +__version__ = "0.15.5" From 12d7913840adf3af4a4e5994a525b3e24c81c3c6 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 4 Jan 2020 14:25:54 -0500 Subject: [PATCH 367/649] Converted api_tests and arrow_tests to pytest --- tests/api_tests.py | 33 +- tests/api_tests.py.bak | 33 + tests/arrow_tests.py | 1497 ++++++++++++--------------- tests/arrow_tests.py.bak | 2081 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 2753 insertions(+), 891 deletions(-) create mode 100644 tests/api_tests.py.bak create mode 100644 tests/arrow_tests.py.bak diff --git a/tests/api_tests.py b/tests/api_tests.py index 1cf171653..9b19a27cd 100644 --- a/tests/api_tests.py +++ b/tests/api_tests.py @@ -1,33 +1,28 @@ # -*- coding: utf-8 -*- -from chai import Chai +import arrow -from arrow import api, arrow, factory +class TestModule: + def test_get(self, mocker): + mocker.patch("arrow.api._factory.get", return_value="result") -class ModuleTests(Chai): - def test_get(self): + assert arrow.api.get() == "result" - self.expect(api._factory.get).args(1, b=2).returns("result") + def test_utcnow(self, mocker): + mocker.patch("arrow.api._factory.utcnow", return_value="utcnow") - self.assertEqual(api.get(1, b=2), "result") + assert arrow.api.utcnow() == "utcnow" - def test_utcnow(self): + def test_now(self, mocker): + mocker.patch("arrow.api._factory.now", tz="tz", return_value="now") - self.expect(api._factory.utcnow).returns("utcnow") - - self.assertEqual(api.utcnow(), "utcnow") - - def test_now(self): - - self.expect(api._factory.now).args("tz").returns("now") - - self.assertEqual(api.now("tz"), "now") + assert arrow.api.now("tz") == "now" def test_factory(self): class MockCustomArrowClass(arrow.Arrow): pass - result = api.factory(MockCustomArrowClass) + result = arrow.api.factory(MockCustomArrowClass) - self.assertIsInstance(result, factory.ArrowFactory) - self.assertIsInstance(result.utcnow(), MockCustomArrowClass) + assert isinstance(result, arrow.factory.ArrowFactory) + assert isinstance(result.utcnow(), MockCustomArrowClass) diff --git a/tests/api_tests.py.bak b/tests/api_tests.py.bak new file mode 100644 index 000000000..1cf171653 --- /dev/null +++ b/tests/api_tests.py.bak @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from chai import Chai + +from arrow import api, arrow, factory + + +class ModuleTests(Chai): + def test_get(self): + + self.expect(api._factory.get).args(1, b=2).returns("result") + + self.assertEqual(api.get(1, b=2), "result") + + def test_utcnow(self): + + self.expect(api._factory.utcnow).returns("utcnow") + + self.assertEqual(api.utcnow(), "utcnow") + + def test_now(self): + + self.expect(api._factory.now).args("tz").returns("now") + + self.assertEqual(api.now("tz"), "now") + + def test_factory(self): + class MockCustomArrowClass(arrow.Arrow): + pass + + result = api.factory(MockCustomArrowClass) + + self.assertIsInstance(result, factory.ArrowFactory) + self.assertIsInstance(result.utcnow(), MockCustomArrowClass) diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index 9d6827660..e9d7cd72e 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -7,9 +7,9 @@ import time from datetime import date, datetime, timedelta +import pytest import pytz import simplejson as json -from chai import Chai from dateutil import tz from dateutil.relativedelta import FR, MO, SA, SU, TH, TU, WE from mock import patch @@ -18,43 +18,43 @@ def assertDtEqual(dt1, dt2, within=10): - assertEqual(dt1.tzinfo, dt2.tzinfo) # noqa: F821 - assertTrue(abs(util.total_seconds(dt1 - dt2)) < within) # noqa: F821 + assert dt1.tzinfo == dt2.tzinfo + assert abs(util.total_seconds(dt1 - dt2)) < within -class ArrowInitTests(Chai): +class TestTestArrowInit: def test_init_bad_input(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): arrow.Arrow(2013) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): arrow.Arrow(2013, 2) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): 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()) - self.assertEqual(result._datetime, self.expected) + assert result._datetime == self.expected result = arrow.Arrow(2013, 2, 2, 12) self.expected = datetime(2013, 2, 2, 12, tzinfo=tz.tzutc()) - self.assertEqual(result._datetime, self.expected) + assert result._datetime == self.expected result = arrow.Arrow(2013, 2, 2, 12, 30) self.expected = datetime(2013, 2, 2, 12, 30, tzinfo=tz.tzutc()) - self.assertEqual(result._datetime, self.expected) + assert result._datetime == self.expected result = arrow.Arrow(2013, 2, 2, 12, 30, 45) self.expected = datetime(2013, 2, 2, 12, 30, 45, tzinfo=tz.tzutc()) - self.assertEqual(result._datetime, self.expected) + assert result._datetime == self.expected result = arrow.Arrow(2013, 2, 2, 12, 30, 45, 999999) self.expected = datetime(2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.tzutc()) - self.assertEqual(result._datetime, self.expected) + assert result._datetime == self.expected result = arrow.Arrow( 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") @@ -62,7 +62,7 @@ def test_init(self): self.expected = datetime( 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") ) - self.assertEqual(result._datetime, self.expected) + assert result._datetime == self.expected # regression tests for issue #626 def test_init_pytz_timezone(self): @@ -73,11 +73,11 @@ def test_init_pytz_timezone(self): self.expected = datetime( 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") ) - self.assertEqual(result._datetime, self.expected) + assert result._datetime == self.expected assertDtEqual(result._datetime, self.expected, 1) -class ArrowFactoryTests(Chai): +class TestTestArrowFactory: def test_now(self): result = arrow.Arrow.now() @@ -109,7 +109,7 @@ def test_fromtimestamp(self): datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), ) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): arrow.Arrow.fromtimestamp("invalid timestamp") def test_utcfromtimestamp(self): @@ -119,7 +119,7 @@ def test_utcfromtimestamp(self): result = arrow.Arrow.utcfromtimestamp(timestamp) assertDtEqual(result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc())) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): arrow.Arrow.utcfromtimestamp("invalid timestamp") def test_fromdatetime(self): @@ -128,7 +128,7 @@ def test_fromdatetime(self): result = arrow.Arrow.fromdatetime(dt) - self.assertEqual(result._datetime, dt.replace(tzinfo=tz.tzutc())) + assert result._datetime == dt.replace(tzinfo=tz.tzutc()) def test_fromdatetime_dt_tzinfo(self): @@ -136,7 +136,7 @@ def test_fromdatetime_dt_tzinfo(self): result = arrow.Arrow.fromdatetime(dt) - self.assertEqual(result._datetime, dt.replace(tzinfo=tz.gettz("US/Pacific"))) + assert result._datetime == dt.replace(tzinfo=tz.gettz("US/Pacific")) def test_fromdatetime_tzinfo_arg(self): @@ -144,7 +144,7 @@ def test_fromdatetime_tzinfo_arg(self): result = arrow.Arrow.fromdatetime(dt, tz.gettz("US/Pacific")) - self.assertEqual(result._datetime, dt.replace(tzinfo=tz.gettz("US/Pacific"))) + assert result._datetime == dt.replace(tzinfo=tz.gettz("US/Pacific")) def test_fromdate(self): @@ -152,94 +152,85 @@ def test_fromdate(self): result = arrow.Arrow.fromdate(dt, tz.gettz("US/Pacific")) - self.assertEqual( - result._datetime, datetime(2013, 2, 3, tzinfo=tz.gettz("US/Pacific")) - ) + 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") - self.assertEqual( - result._datetime, datetime(2013, 2, 3, 12, 30, 45, tzinfo=tz.tzutc()) - ) + 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") ) - self.assertEqual( - result._datetime, - datetime(2013, 2, 3, 12, 30, 45, tzinfo=tz.gettz("Europe/Paris")), + assert result._datetime == datetime( + 2013, 2, 3, 12, 30, 45, tzinfo=tz.gettz("Europe/Paris") ) -class ArrowRepresentationTests(Chai): - def setUp(self): - super(ArrowRepresentationTests, self).setUp() - - self.arrow = arrow.Arrow(2013, 2, 3, 12, 30, 45, 1) +class TestTestArrowRepresentation: + @classmethod + def setup_class(cls): + cls.arrow = arrow.Arrow(2013, 2, 3, 12, 30, 45, 1) def test_repr(self): result = self.arrow.__repr__() - self.assertEqual( - result, "".format(self.arrow._datetime.isoformat()) - ) + assert result == "".format(self.arrow._datetime.isoformat()) def test_str(self): result = self.arrow.__str__() - self.assertEqual(result, self.arrow._datetime.isoformat()) + assert result == self.arrow._datetime.isoformat() def test_hash(self): result = self.arrow.__hash__() - self.assertEqual(result, self.arrow._datetime.__hash__()) + assert result == self.arrow._datetime.__hash__() def test_format(self): result = "{:YYYY-MM-DD}".format(self.arrow) - self.assertEqual(result, "2013-02-03") + assert result == "2013-02-03" def test_bare_format(self): result = self.arrow.format() - self.assertEqual(result, "2013-02-03 12:30:45+00:00") + assert result == "2013-02-03 12:30:45+00:00" def test_format_no_format_string(self): result = "{}".format(self.arrow) - self.assertEqual(result, str(self.arrow)) + assert result == str(self.arrow) def test_clone(self): result = self.arrow.clone() - self.assertTrue(result is not self.arrow) - self.assertEqual(result._datetime, self.arrow._datetime) - + assert result is not self.arrow + assert result._datetime == self.arrow._datetime -class ArrowAttributeTests(Chai): - def setUp(self): - super(ArrowAttributeTests, self).setUp() - self.arrow = arrow.Arrow(2013, 1, 1) +class TestArrowAttribute: + @classmethod + def setup_class(cls): + cls.arrow = arrow.Arrow(2013, 1, 1) def test_getattr_base(self): - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): self.arrow.prop def test_getattr_week(self): - self.assertEqual(self.arrow.week, 1) + assert self.arrow.week == 1 def test_getattr_quarter(self): # start dates @@ -247,189 +238,186 @@ def test_getattr_quarter(self): q2 = arrow.Arrow(2013, 4, 1) q3 = arrow.Arrow(2013, 8, 1) q4 = arrow.Arrow(2013, 10, 1) - self.assertEqual(q1.quarter, 1) - self.assertEqual(q2.quarter, 2) - self.assertEqual(q3.quarter, 3) - self.assertEqual(q4.quarter, 4) + assert q1.quarter == 1 + assert q2.quarter == 2 + assert q3.quarter == 3 + assert q4.quarter == 4 # end dates q1 = arrow.Arrow(2013, 3, 31) q2 = arrow.Arrow(2013, 6, 30) q3 = arrow.Arrow(2013, 9, 30) q4 = arrow.Arrow(2013, 12, 31) - self.assertEqual(q1.quarter, 1) - self.assertEqual(q2.quarter, 2) - self.assertEqual(q3.quarter, 3) - self.assertEqual(q4.quarter, 4) + assert q1.quarter == 1 + assert q2.quarter == 2 + assert q3.quarter == 3 + assert q4.quarter == 4 def test_getattr_dt_value(self): - self.assertEqual(self.arrow.year, 2013) + assert self.arrow.year == 2013 def test_tzinfo(self): self.arrow.tzinfo = tz.gettz("PST") - self.assertEqual(self.arrow.tzinfo, tz.gettz("PST")) + assert self.arrow.tzinfo == tz.gettz("PST") def test_naive(self): - self.assertEqual(self.arrow.naive, self.arrow._datetime.replace(tzinfo=None)) + assert self.arrow.naive == self.arrow._datetime.replace(tzinfo=None) def test_timestamp(self): - self.assertEqual( - self.arrow.timestamp, calendar.timegm(self.arrow._datetime.utctimetuple()) + assert self.arrow.timestamp == calendar.timegm( + self.arrow._datetime.utctimetuple() ) def test_float_timestamp(self): result = self.arrow.float_timestamp - self.arrow.timestamp - self.assertEqual(result, self.arrow.microsecond) - + assert result == self.arrow.microsecond -class ArrowComparisonTests(Chai): - def setUp(self): - super(ArrowComparisonTests, self).setUp() - self.arrow = arrow.Arrow.utcnow() +class TestArrowComparison: + @classmethod + def setup_class(cls): + cls.arrow = arrow.Arrow.utcnow() def test_eq(self): - self.assertTrue(self.arrow == self.arrow) - self.assertTrue(self.arrow == self.arrow.datetime) - self.assertFalse(self.arrow == "abc") + assert self.arrow == self.arrow + assert self.arrow == self.arrow.datetime + assert not (self.arrow == "abc") def test_ne(self): - self.assertFalse(self.arrow != self.arrow) - self.assertFalse(self.arrow != self.arrow.datetime) - self.assertTrue(self.arrow != "abc") + 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) - self.assertFalse(self.arrow > self.arrow) - self.assertFalse(self.arrow > self.arrow.datetime) + assert not (self.arrow > self.arrow) + assert not (self.arrow > self.arrow.datetime) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.arrow > "abc" - self.assertTrue(self.arrow < arrow_cmp) - self.assertTrue(self.arrow < arrow_cmp.datetime) + assert self.arrow < arrow_cmp + assert self.arrow < arrow_cmp.datetime def test_ge(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.arrow >= "abc" - self.assertTrue(self.arrow >= self.arrow) - self.assertTrue(self.arrow >= self.arrow.datetime) + assert self.arrow >= self.arrow + assert self.arrow >= self.arrow.datetime def test_lt(self): arrow_cmp = self.arrow.shift(minutes=1) - self.assertFalse(self.arrow < self.arrow) - self.assertFalse(self.arrow < self.arrow.datetime) + assert not (self.arrow < self.arrow) + assert not (self.arrow < self.arrow.datetime) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.arrow < "abc" - self.assertTrue(self.arrow < arrow_cmp) - self.assertTrue(self.arrow < arrow_cmp.datetime) + assert self.arrow < arrow_cmp + assert self.arrow < arrow_cmp.datetime def test_le(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.arrow <= "abc" - self.assertTrue(self.arrow <= self.arrow) - self.assertTrue(self.arrow <= self.arrow.datetime) + assert self.arrow <= self.arrow + assert self.arrow <= self.arrow.datetime -class ArrowMathTests(Chai): - def setUp(self): - super(ArrowMathTests, self).setUp() - - self.arrow = arrow.Arrow(2013, 1, 1) +class TestArrowMath: + @classmethod + def setup_class(cls): + cls.arrow = arrow.Arrow(2013, 1, 1) def test_add_timedelta(self): result = self.arrow.__add__(timedelta(days=1)) - self.assertEqual(result._datetime, datetime(2013, 1, 2, tzinfo=tz.tzutc())) + assert result._datetime == datetime(2013, 1, 2, tzinfo=tz.tzutc()) def test_add_other(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.arrow + 1 def test_radd(self): result = self.arrow.__radd__(timedelta(days=1)) - self.assertEqual(result._datetime, datetime(2013, 1, 2, tzinfo=tz.tzutc())) + assert result._datetime == datetime(2013, 1, 2, tzinfo=tz.tzutc()) def test_sub_timedelta(self): result = self.arrow.__sub__(timedelta(days=1)) - self.assertEqual(result._datetime, datetime(2012, 12, 31, tzinfo=tz.tzutc())) + 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())) - self.assertEqual(result, timedelta(days=11)) + assert result == timedelta(days=11) def test_sub_arrow(self): result = self.arrow.__sub__(arrow.Arrow(2012, 12, 21, tzinfo=tz.tzutc())) - self.assertEqual(result, timedelta(days=11)) + assert result == timedelta(days=11) def test_sub_other(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.arrow - object() def test_rsub_datetime(self): result = self.arrow.__rsub__(datetime(2012, 12, 21, tzinfo=tz.tzutc())) - self.assertEqual(result, timedelta(days=-11)) + assert result == timedelta(days=-11) def test_rsub_other(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): timedelta(days=1) - self.arrow -class ArrowDatetimeInterfaceTests(Chai): - def setUp(self): - super(ArrowDatetimeInterfaceTests, self).setUp() - - self.arrow = arrow.Arrow.utcnow() +class TestArrowDatetimeInterface: + @classmethod + def setup_class(cls): + cls.arrow = arrow.Arrow.utcnow() def test_date(self): result = self.arrow.date() - self.assertEqual(result, self.arrow._datetime.date()) + assert result == self.arrow._datetime.date() def test_time(self): result = self.arrow.time() - self.assertEqual(result, self.arrow._datetime.time()) + assert result == self.arrow._datetime.time() def test_timetz(self): result = self.arrow.timetz() - self.assertEqual(result, self.arrow._datetime.timetz()) + assert result == self.arrow._datetime.timetz() def test_astimezone(self): @@ -437,82 +425,82 @@ def test_astimezone(self): result = self.arrow.astimezone(other_tz) - self.assertEqual(result, self.arrow._datetime.astimezone(other_tz)) + assert result == self.arrow._datetime.astimezone(other_tz) def test_utcoffset(self): result = self.arrow.utcoffset() - self.assertEqual(result, self.arrow._datetime.utcoffset()) + assert result == self.arrow._datetime.utcoffset() def test_dst(self): result = self.arrow.dst() - self.assertEqual(result, self.arrow._datetime.dst()) + assert result == self.arrow._datetime.dst() def test_timetuple(self): result = self.arrow.timetuple() - self.assertEqual(result, self.arrow._datetime.timetuple()) + assert result == self.arrow._datetime.timetuple() def test_utctimetuple(self): result = self.arrow.utctimetuple() - self.assertEqual(result, self.arrow._datetime.utctimetuple()) + assert result == self.arrow._datetime.utctimetuple() def test_toordinal(self): result = self.arrow.toordinal() - self.assertEqual(result, self.arrow._datetime.toordinal()) + assert result == self.arrow._datetime.toordinal() def test_weekday(self): result = self.arrow.weekday() - self.assertEqual(result, self.arrow._datetime.weekday()) + assert result == self.arrow._datetime.weekday() def test_isoweekday(self): result = self.arrow.isoweekday() - self.assertEqual(result, self.arrow._datetime.isoweekday()) + assert result == self.arrow._datetime.isoweekday() def test_isocalendar(self): result = self.arrow.isocalendar() - self.assertEqual(result, self.arrow._datetime.isocalendar()) + assert result == self.arrow._datetime.isocalendar() def test_isoformat(self): result = self.arrow.isoformat() - self.assertEqual(result, self.arrow._datetime.isoformat()) + assert result == self.arrow._datetime.isoformat() def test_simplejson(self): result = json.dumps({"v": self.arrow.for_json()}, for_json=True) - self.assertEqual(json.loads(result)["v"], self.arrow._datetime.isoformat()) + assert json.loads(result)["v"] == self.arrow._datetime.isoformat() def test_ctime(self): result = self.arrow.ctime() - self.assertEqual(result, self.arrow._datetime.ctime()) + assert result == self.arrow._datetime.ctime() def test_strftime(self): result = self.arrow.strftime("%Y") - self.assertEqual(result, self.arrow._datetime.strftime("%Y")) + assert result == self.arrow._datetime.strftime("%Y") -class ArrowFalsePositiveDstTests(Chai): +class TestArrowFalsePositiveDst: """These tests relate to issues #376 and #551. The key points in both issues are that arrow will assign a UTC timezone if none is provided and .to() will change other attributes to be correct whereas .replace() only changes the specified attribute. @@ -536,35 +524,34 @@ class ArrowFalsePositiveDstTests(Chai): 2018-11-04T02:00:00-05:00 """ - def setUp(self): - - super(ArrowFalsePositiveDstTests, self).setUp() - self.before_1 = arrow.Arrow( + @classmethod + def setup_class(cls): + cls.before_1 = arrow.Arrow( 2016, 11, 6, 3, 59, tzinfo=tz.gettz("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.after_2 = arrow.Arrow( + cls.before_2 = arrow.Arrow(2016, 11, 6, tzinfo=tz.gettz("America/New_York")) + cls.after_1 = arrow.Arrow(2016, 11, 6, 4, tzinfo=tz.gettz("America/New_York")) + cls.after_2 = arrow.Arrow( 2016, 11, 6, 23, 59, tzinfo=tz.gettz("America/New_York") ) - self.before_3 = arrow.Arrow( + cls.before_3 = arrow.Arrow( 2018, 11, 4, 3, 59, tzinfo=tz.gettz("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.after_4 = arrow.Arrow( + cls.before_4 = arrow.Arrow(2018, 11, 4, tzinfo=tz.gettz("America/New_York")) + cls.after_3 = arrow.Arrow(2018, 11, 4, 4, tzinfo=tz.gettz("America/New_York")) + cls.after_4 = arrow.Arrow( 2018, 11, 4, 23, 59, tzinfo=tz.gettz("America/New_York") ) def test_dst(self): - self.assertEqual(self.before_1.day, self.before_2.day) - self.assertEqual(self.after_1.day, self.after_2.day) - self.assertEqual(self.before_3.day, self.before_4.day) - self.assertEqual(self.after_3.day, self.after_4.day) + assert self.before_1.day == self.before_2.day + assert self.after_1.day == self.after_2.day + assert self.before_3.day == self.before_4.day + assert self.after_3.day == self.after_4.day -class ArrowConversionTests(Chai): +class TestArrowConversion: def test_to(self): dt_from = datetime.now() @@ -574,11 +561,11 @@ def test_to(self): tz.tzutc() ) - self.assertEqual(arrow_from.to("UTC").datetime, self.expected) - self.assertEqual(arrow_from.to(tz.tzutc()).datetime, self.expected) + assert arrow_from.to("UTC").datetime == self.expected + assert arrow_from.to(tz.tzutc()).datetime == self.expected -class ArrowPicklingTests(Chai): +class TestArrowPickling: def test_pickle_and_unpickle(self): dt = arrow.Arrow.utcnow() @@ -587,25 +574,25 @@ def test_pickle_and_unpickle(self): unpickled = pickle.loads(pickled) - self.assertEqual(unpickled, dt) + assert unpickled == dt -class ArrowReplaceTests(Chai): +class TestArrowReplace: def test_not_attr(self): - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): arrow.Arrow.utcnow().replace(abc=1) def test_replace(self): arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) - self.assertEqual(arw.replace(year=2012), arrow.Arrow(2012, 5, 5, 12, 30, 45)) - self.assertEqual(arw.replace(month=1), arrow.Arrow(2013, 1, 5, 12, 30, 45)) - self.assertEqual(arw.replace(day=1), arrow.Arrow(2013, 5, 1, 12, 30, 45)) - self.assertEqual(arw.replace(hour=1), arrow.Arrow(2013, 5, 5, 1, 30, 45)) - self.assertEqual(arw.replace(minute=1), arrow.Arrow(2013, 5, 5, 12, 1, 45)) - self.assertEqual(arw.replace(second=1), arrow.Arrow(2013, 5, 5, 12, 30, 1)) + assert arw.replace(year=2012) == arrow.Arrow(2012, 5, 5, 12, 30, 45) + assert arw.replace(month=1) == arrow.Arrow(2013, 1, 5, 12, 30, 45) + assert arw.replace(day=1) == arrow.Arrow(2013, 5, 1, 12, 30, 45) + assert arw.replace(hour=1) == arrow.Arrow(2013, 5, 5, 1, 30, 45) + assert arw.replace(minute=1) == arrow.Arrow(2013, 5, 5, 12, 1, 45) + assert arw.replace(second=1) == arrow.Arrow(2013, 5, 5, 12, 30, 1) def test_replace_tzinfo(self): @@ -613,141 +600,127 @@ def test_replace_tzinfo(self): result = arw.replace(tzinfo=tz.gettz("US/Pacific")) - self.assertEqual(result, arw.datetime.replace(tzinfo=tz.gettz("US/Pacific"))) + assert result == arw.datetime.replace(tzinfo=tz.gettz("US/Pacific")) def test_replace_week(self): - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): arrow.Arrow.utcnow().replace(week=1) def test_replace_quarter(self): - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): arrow.Arrow.utcnow().replace(quarter=1) def test_replace_other_kwargs(self): - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): arrow.utcnow().replace(abc="def") -class ArrowShiftTests(Chai): +class TestArrowShift: def test_not_attr(self): now = arrow.Arrow.utcnow() - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): now.shift(abc=1) - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): now.shift(week=1) def test_shift(self): arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) - self.assertEqual(arw.shift(years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) - self.assertEqual(arw.shift(quarters=1), arrow.Arrow(2013, 8, 5, 12, 30, 45)) - self.assertEqual( - arw.shift(quarters=1, months=1), arrow.Arrow(2013, 9, 5, 12, 30, 45) - ) - self.assertEqual(arw.shift(months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) - self.assertEqual(arw.shift(weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) - self.assertEqual(arw.shift(days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - self.assertEqual(arw.shift(hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) - self.assertEqual(arw.shift(minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) - self.assertEqual(arw.shift(seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) - self.assertEqual( - arw.shift(microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1) - ) + assert arw.shift(years=1) == arrow.Arrow(2014, 5, 5, 12, 30, 45) + assert arw.shift(quarters=1) == arrow.Arrow(2013, 8, 5, 12, 30, 45) + assert arw.shift(quarters=1, months=1) == arrow.Arrow(2013, 9, 5, 12, 30, 45) + assert arw.shift(months=1) == arrow.Arrow(2013, 6, 5, 12, 30, 45) + assert arw.shift(weeks=1) == arrow.Arrow(2013, 5, 12, 12, 30, 45) + assert arw.shift(days=1) == arrow.Arrow(2013, 5, 6, 12, 30, 45) + assert arw.shift(hours=1) == arrow.Arrow(2013, 5, 5, 13, 30, 45) + assert arw.shift(minutes=1) == arrow.Arrow(2013, 5, 5, 12, 31, 45) + assert arw.shift(seconds=1) == arrow.Arrow(2013, 5, 5, 12, 30, 46) + assert arw.shift(microseconds=1) == arrow.Arrow(2013, 5, 5, 12, 30, 45, 1) # Remember: Python's weekday 0 is Monday - self.assertEqual(arw.shift(weekday=0), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=1), arrow.Arrow(2013, 5, 7, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=2), arrow.Arrow(2013, 5, 8, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=3), arrow.Arrow(2013, 5, 9, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=4), arrow.Arrow(2013, 5, 10, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=5), arrow.Arrow(2013, 5, 11, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=6), arw) - - with self.assertRaises(IndexError): + assert arw.shift(weekday=0) == arrow.Arrow(2013, 5, 6, 12, 30, 45) + assert arw.shift(weekday=1) == arrow.Arrow(2013, 5, 7, 12, 30, 45) + assert arw.shift(weekday=2) == arrow.Arrow(2013, 5, 8, 12, 30, 45) + assert arw.shift(weekday=3) == arrow.Arrow(2013, 5, 9, 12, 30, 45) + assert arw.shift(weekday=4) == arrow.Arrow(2013, 5, 10, 12, 30, 45) + assert arw.shift(weekday=5) == arrow.Arrow(2013, 5, 11, 12, 30, 45) + assert arw.shift(weekday=6) == arw + + with pytest.raises(IndexError): arw.shift(weekday=7) # Use dateutil.relativedelta's convenient day instances - self.assertEqual(arw.shift(weekday=MO), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=MO(0)), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=MO(1)), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=MO(2)), arrow.Arrow(2013, 5, 13, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TU), arrow.Arrow(2013, 5, 7, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TU(0)), arrow.Arrow(2013, 5, 7, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TU(1)), arrow.Arrow(2013, 5, 7, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TU(2)), arrow.Arrow(2013, 5, 14, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=WE), arrow.Arrow(2013, 5, 8, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=WE(0)), arrow.Arrow(2013, 5, 8, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=WE(1)), arrow.Arrow(2013, 5, 8, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=WE(2)), arrow.Arrow(2013, 5, 15, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TH), arrow.Arrow(2013, 5, 9, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TH(0)), arrow.Arrow(2013, 5, 9, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TH(1)), arrow.Arrow(2013, 5, 9, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TH(2)), arrow.Arrow(2013, 5, 16, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=FR), arrow.Arrow(2013, 5, 10, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=FR(0)), arrow.Arrow(2013, 5, 10, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=FR(1)), arrow.Arrow(2013, 5, 10, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=FR(2)), arrow.Arrow(2013, 5, 17, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=SA), arrow.Arrow(2013, 5, 11, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=SA(0)), arrow.Arrow(2013, 5, 11, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=SA(1)), arrow.Arrow(2013, 5, 11, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=SA(2)), arrow.Arrow(2013, 5, 18, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=SU), arw) - self.assertEqual(arw.shift(weekday=SU(0)), arw) - self.assertEqual(arw.shift(weekday=SU(1)), arw) - self.assertEqual(arw.shift(weekday=SU(2)), arrow.Arrow(2013, 5, 12, 12, 30, 45)) + assert arw.shift(weekday=MO) == arrow.Arrow(2013, 5, 6, 12, 30, 45) + assert arw.shift(weekday=MO(0)) == arrow.Arrow(2013, 5, 6, 12, 30, 45) + assert arw.shift(weekday=MO(1)) == arrow.Arrow(2013, 5, 6, 12, 30, 45) + assert arw.shift(weekday=MO(2)) == arrow.Arrow(2013, 5, 13, 12, 30, 45) + assert arw.shift(weekday=TU) == arrow.Arrow(2013, 5, 7, 12, 30, 45) + assert arw.shift(weekday=TU(0)) == arrow.Arrow(2013, 5, 7, 12, 30, 45) + assert arw.shift(weekday=TU(1)) == arrow.Arrow(2013, 5, 7, 12, 30, 45) + assert arw.shift(weekday=TU(2)) == arrow.Arrow(2013, 5, 14, 12, 30, 45) + assert arw.shift(weekday=WE) == arrow.Arrow(2013, 5, 8, 12, 30, 45) + assert arw.shift(weekday=WE(0)) == arrow.Arrow(2013, 5, 8, 12, 30, 45) + assert arw.shift(weekday=WE(1)) == arrow.Arrow(2013, 5, 8, 12, 30, 45) + assert arw.shift(weekday=WE(2)) == arrow.Arrow(2013, 5, 15, 12, 30, 45) + assert arw.shift(weekday=TH) == arrow.Arrow(2013, 5, 9, 12, 30, 45) + assert arw.shift(weekday=TH(0)) == arrow.Arrow(2013, 5, 9, 12, 30, 45) + assert arw.shift(weekday=TH(1)) == arrow.Arrow(2013, 5, 9, 12, 30, 45) + assert arw.shift(weekday=TH(2)) == arrow.Arrow(2013, 5, 16, 12, 30, 45) + assert arw.shift(weekday=FR) == arrow.Arrow(2013, 5, 10, 12, 30, 45) + assert arw.shift(weekday=FR(0)) == arrow.Arrow(2013, 5, 10, 12, 30, 45) + assert arw.shift(weekday=FR(1)) == arrow.Arrow(2013, 5, 10, 12, 30, 45) + assert arw.shift(weekday=FR(2)) == arrow.Arrow(2013, 5, 17, 12, 30, 45) + assert arw.shift(weekday=SA) == arrow.Arrow(2013, 5, 11, 12, 30, 45) + assert arw.shift(weekday=SA(0)) == arrow.Arrow(2013, 5, 11, 12, 30, 45) + assert arw.shift(weekday=SA(1)) == arrow.Arrow(2013, 5, 11, 12, 30, 45) + assert arw.shift(weekday=SA(2)) == arrow.Arrow(2013, 5, 18, 12, 30, 45) + assert arw.shift(weekday=SU) == arw + assert arw.shift(weekday=SU(0)) == arw + assert arw.shift(weekday=SU(1)) == arw + 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) - self.assertEqual(arw.shift(years=-1), arrow.Arrow(2012, 5, 5, 12, 30, 45)) - self.assertEqual(arw.shift(quarters=-1), arrow.Arrow(2013, 2, 5, 12, 30, 45)) - self.assertEqual( - arw.shift(quarters=-1, months=-1), arrow.Arrow(2013, 1, 5, 12, 30, 45) - ) - self.assertEqual(arw.shift(months=-1), arrow.Arrow(2013, 4, 5, 12, 30, 45)) - self.assertEqual(arw.shift(weeks=-1), arrow.Arrow(2013, 4, 28, 12, 30, 45)) - self.assertEqual(arw.shift(days=-1), arrow.Arrow(2013, 5, 4, 12, 30, 45)) - self.assertEqual(arw.shift(hours=-1), arrow.Arrow(2013, 5, 5, 11, 30, 45)) - self.assertEqual(arw.shift(minutes=-1), arrow.Arrow(2013, 5, 5, 12, 29, 45)) - self.assertEqual(arw.shift(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) - self.assertEqual( - arw.shift(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999) - ) + assert arw.shift(years=-1) == arrow.Arrow(2012, 5, 5, 12, 30, 45) + assert arw.shift(quarters=-1) == arrow.Arrow(2013, 2, 5, 12, 30, 45) + assert arw.shift(quarters=-1, months=-1) == arrow.Arrow(2013, 1, 5, 12, 30, 45) + assert arw.shift(months=-1) == arrow.Arrow(2013, 4, 5, 12, 30, 45) + assert arw.shift(weeks=-1) == arrow.Arrow(2013, 4, 28, 12, 30, 45) + assert arw.shift(days=-1) == arrow.Arrow(2013, 5, 4, 12, 30, 45) + assert arw.shift(hours=-1) == arrow.Arrow(2013, 5, 5, 11, 30, 45) + assert arw.shift(minutes=-1) == arrow.Arrow(2013, 5, 5, 12, 29, 45) + assert arw.shift(seconds=-1) == arrow.Arrow(2013, 5, 5, 12, 30, 44) + assert arw.shift(microseconds=-1) == arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999) # Not sure how practical these negative weekdays are - self.assertEqual(arw.shift(weekday=-1), arw.shift(weekday=SU)) - self.assertEqual(arw.shift(weekday=-2), arw.shift(weekday=SA)) - self.assertEqual(arw.shift(weekday=-3), arw.shift(weekday=FR)) - self.assertEqual(arw.shift(weekday=-4), arw.shift(weekday=TH)) - self.assertEqual(arw.shift(weekday=-5), arw.shift(weekday=WE)) - self.assertEqual(arw.shift(weekday=-6), arw.shift(weekday=TU)) - self.assertEqual(arw.shift(weekday=-7), arw.shift(weekday=MO)) - - with self.assertRaises(IndexError): + assert arw.shift(weekday=-1) == arw.shift(weekday=SU) + assert arw.shift(weekday=-2) == arw.shift(weekday=SA) + assert arw.shift(weekday=-3) == arw.shift(weekday=FR) + assert arw.shift(weekday=-4) == arw.shift(weekday=TH) + assert arw.shift(weekday=-5) == arw.shift(weekday=WE) + assert arw.shift(weekday=-6) == arw.shift(weekday=TU) + assert arw.shift(weekday=-7) == arw.shift(weekday=MO) + + with pytest.raises(IndexError): arw.shift(weekday=-8) - self.assertEqual( - arw.shift(weekday=MO(-1)), arrow.Arrow(2013, 4, 29, 12, 30, 45) - ) - self.assertEqual( - arw.shift(weekday=TU(-1)), arrow.Arrow(2013, 4, 30, 12, 30, 45) - ) - self.assertEqual(arw.shift(weekday=WE(-1)), arrow.Arrow(2013, 5, 1, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TH(-1)), arrow.Arrow(2013, 5, 2, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=FR(-1)), arrow.Arrow(2013, 5, 3, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=SA(-1)), arrow.Arrow(2013, 5, 4, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=SU(-1)), arw) - self.assertEqual( - arw.shift(weekday=SU(-2)), arrow.Arrow(2013, 4, 28, 12, 30, 45) - ) + assert arw.shift(weekday=MO(-1)) == arrow.Arrow(2013, 4, 29, 12, 30, 45) + assert arw.shift(weekday=TU(-1)) == arrow.Arrow(2013, 4, 30, 12, 30, 45) + assert arw.shift(weekday=WE(-1)) == arrow.Arrow(2013, 5, 1, 12, 30, 45) + assert arw.shift(weekday=TH(-1)) == arrow.Arrow(2013, 5, 2, 12, 30, 45) + assert arw.shift(weekday=FR(-1)) == arrow.Arrow(2013, 5, 3, 12, 30, 45) + assert arw.shift(weekday=SA(-1)) == arrow.Arrow(2013, 5, 4, 12, 30, 45) + assert arw.shift(weekday=SU(-1)) == arw + assert arw.shift(weekday=SU(-2)) == arrow.Arrow(2013, 4, 28, 12, 30, 45) def test_shift_quarters_bug(self): @@ -756,34 +729,19 @@ def test_shift_quarters_bug(self): # The value of the last-read argument was used instead of the ``quarters`` argument. # Recall that the keyword argument dict, like all dicts, is unordered, so only certain # combinations of arguments would exhibit this. - self.assertEqual( - arw.shift(quarters=0, years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45) - ) - self.assertEqual( - arw.shift(quarters=0, months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45) - ) - self.assertEqual( - arw.shift(quarters=0, weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45) - ) - self.assertEqual( - arw.shift(quarters=0, days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45) - ) - self.assertEqual( - arw.shift(quarters=0, hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45) - ) - self.assertEqual( - arw.shift(quarters=0, minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45) - ) - self.assertEqual( - arw.shift(quarters=0, seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46) - ) - self.assertEqual( - arw.shift(quarters=0, microseconds=1), - arrow.Arrow(2013, 5, 5, 12, 30, 45, 1), + assert arw.shift(quarters=0, years=1) == arrow.Arrow(2014, 5, 5, 12, 30, 45) + assert arw.shift(quarters=0, months=1) == arrow.Arrow(2013, 6, 5, 12, 30, 45) + assert arw.shift(quarters=0, weeks=1) == arrow.Arrow(2013, 5, 12, 12, 30, 45) + assert arw.shift(quarters=0, days=1) == arrow.Arrow(2013, 5, 6, 12, 30, 45) + assert arw.shift(quarters=0, hours=1) == arrow.Arrow(2013, 5, 5, 13, 30, 45) + assert arw.shift(quarters=0, minutes=1) == arrow.Arrow(2013, 5, 5, 12, 31, 45) + assert arw.shift(quarters=0, seconds=1) == arrow.Arrow(2013, 5, 5, 12, 30, 46) + assert arw.shift(quarters=0, microseconds=1) == arrow.Arrow( + 2013, 5, 5, 12, 30, 45, 1 ) -class ArrowRangeTests(Chai): +class TestArrowRange: def test_year(self): result = list( @@ -792,15 +750,12 @@ def test_year(self): ) ) - self.assertEqual( - result, - [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2014, 1, 2, 3, 4, 5), - arrow.Arrow(2015, 1, 2, 3, 4, 5), - arrow.Arrow(2016, 1, 2, 3, 4, 5), - ], - ) + assert result == [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2014, 1, 2, 3, 4, 5), + arrow.Arrow(2015, 1, 2, 3, 4, 5), + arrow.Arrow(2016, 1, 2, 3, 4, 5), + ] def test_quarter(self): @@ -810,9 +765,10 @@ def test_quarter(self): ) ) - self.assertEqual( - result, [arrow.Arrow(2013, 2, 3, 4, 5, 6), arrow.Arrow(2013, 5, 3, 4, 5, 6)] - ) + assert result == [ + arrow.Arrow(2013, 2, 3, 4, 5, 6), + arrow.Arrow(2013, 5, 3, 4, 5, 6), + ] def test_month(self): @@ -822,15 +778,12 @@ def test_month(self): ) ) - self.assertEqual( - result, - [ - arrow.Arrow(2013, 2, 3, 4, 5, 6), - arrow.Arrow(2013, 3, 3, 4, 5, 6), - arrow.Arrow(2013, 4, 3, 4, 5, 6), - arrow.Arrow(2013, 5, 3, 4, 5, 6), - ], - ) + assert result == [ + arrow.Arrow(2013, 2, 3, 4, 5, 6), + arrow.Arrow(2013, 3, 3, 4, 5, 6), + arrow.Arrow(2013, 4, 3, 4, 5, 6), + arrow.Arrow(2013, 5, 3, 4, 5, 6), + ] def test_week(self): @@ -840,16 +793,13 @@ def test_week(self): ) ) - self.assertEqual( - result, - [ - arrow.Arrow(2013, 9, 1, 2, 3, 4), - arrow.Arrow(2013, 9, 8, 2, 3, 4), - arrow.Arrow(2013, 9, 15, 2, 3, 4), - arrow.Arrow(2013, 9, 22, 2, 3, 4), - arrow.Arrow(2013, 9, 29, 2, 3, 4), - ], - ) + assert result == [ + arrow.Arrow(2013, 9, 1, 2, 3, 4), + arrow.Arrow(2013, 9, 8, 2, 3, 4), + arrow.Arrow(2013, 9, 15, 2, 3, 4), + arrow.Arrow(2013, 9, 22, 2, 3, 4), + arrow.Arrow(2013, 9, 29, 2, 3, 4), + ] def test_day(self): @@ -859,15 +809,12 @@ def test_day(self): ) ) - self.assertEqual( - result, - [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 3, 3, 4, 5), - arrow.Arrow(2013, 1, 4, 3, 4, 5), - arrow.Arrow(2013, 1, 5, 3, 4, 5), - ], - ) + assert result == [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 3, 3, 4, 5), + arrow.Arrow(2013, 1, 4, 3, 4, 5), + arrow.Arrow(2013, 1, 5, 3, 4, 5), + ] def test_hour(self): @@ -877,15 +824,12 @@ def test_hour(self): ) ) - self.assertEqual( - result, - [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 2, 4, 4, 5), - arrow.Arrow(2013, 1, 2, 5, 4, 5), - arrow.Arrow(2013, 1, 2, 6, 4, 5), - ], - ) + assert result == [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 2, 4, 4, 5), + arrow.Arrow(2013, 1, 2, 5, 4, 5), + arrow.Arrow(2013, 1, 2, 6, 4, 5), + ] result = list( arrow.Arrow.range( @@ -893,7 +837,7 @@ def test_hour(self): ) ) - self.assertEqual(result, [arrow.Arrow(2013, 1, 2, 3, 4, 5)]) + assert result == [arrow.Arrow(2013, 1, 2, 3, 4, 5)] def test_minute(self): @@ -903,15 +847,12 @@ def test_minute(self): ) ) - self.assertEqual( - result, - [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 2, 3, 5, 5), - arrow.Arrow(2013, 1, 2, 3, 6, 5), - arrow.Arrow(2013, 1, 2, 3, 7, 5), - ], - ) + assert result == [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 2, 3, 5, 5), + arrow.Arrow(2013, 1, 2, 3, 6, 5), + arrow.Arrow(2013, 1, 2, 3, 7, 5), + ] def test_second(self): @@ -921,15 +862,12 @@ def test_second(self): ) ) - self.assertEqual( - result, - [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 2, 3, 4, 6), - arrow.Arrow(2013, 1, 2, 3, 4, 7), - arrow.Arrow(2013, 1, 2, 3, 4, 8), - ], - ) + assert result == [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 2, 3, 4, 6), + arrow.Arrow(2013, 1, 2, 3, 4, 7), + arrow.Arrow(2013, 1, 2, 3, 4, 8), + ] def test_arrow(self): @@ -941,15 +879,12 @@ def test_arrow(self): ) ) - self.assertEqual( - result, - [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 3, 3, 4, 5), - arrow.Arrow(2013, 1, 4, 3, 4, 5), - arrow.Arrow(2013, 1, 5, 3, 4, 5), - ], - ) + assert result == [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 3, 3, 4, 5), + arrow.Arrow(2013, 1, 4, 3, 4, 5), + arrow.Arrow(2013, 1, 5, 3, 4, 5), + ] def test_naive_tz(self): @@ -957,7 +892,8 @@ def test_naive_tz(self): "year", datetime(2013, 1, 2, 3), datetime(2016, 4, 5, 6), "US/Pacific" ) - [self.assertEqual(r.tzinfo, tz.gettz("US/Pacific")) for r in result] + for r in result: + assert r.tzinfo == tz.gettz("US/Pacific") def test_aware_same_tz(self): @@ -967,7 +903,8 @@ def test_aware_same_tz(self): arrow.Arrow(2013, 1, 3, tzinfo=tz.gettz("US/Pacific")), ) - [self.assertEqual(r.tzinfo, tz.gettz("US/Pacific")) for r in result] + for r in result: + assert r.tzinfo == tz.gettz("US/Pacific") def test_aware_different_tz(self): @@ -977,7 +914,8 @@ def test_aware_different_tz(self): datetime(2013, 1, 3, tzinfo=tz.gettz("US/Pacific")), ) - [self.assertEqual(r.tzinfo, tz.gettz("US/Eastern")) for r in result] + for r in result: + assert r.tzinfo == tz.gettz("US/Eastern") def test_aware_tz(self): @@ -988,42 +926,28 @@ def test_aware_tz(self): tz=tz.gettz("US/Central"), ) - [self.assertEqual(r.tzinfo, tz.gettz("US/Central")) for r in result] + for r in result: + assert r.tzinfo == tz.gettz("US/Central") def test_unsupported(self): - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): next(arrow.Arrow.range("abc", datetime.utcnow(), datetime.utcnow())) -class ArrowSpanRangeTests(Chai): +class TestArrowSpanRange: def test_year(self): result = list( arrow.Arrow.span_range("year", datetime(2013, 2, 1), datetime(2016, 3, 31)) ) - self.assertEqual( - result, - [ - ( - arrow.Arrow(2013, 1, 1), - arrow.Arrow(2013, 12, 31, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2014, 1, 1), - arrow.Arrow(2014, 12, 31, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2015, 1, 1), - arrow.Arrow(2015, 12, 31, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2016, 1, 1), - arrow.Arrow(2016, 12, 31, 23, 59, 59, 999999), - ), - ], - ) + assert result == [ + (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 12, 31, 23, 59, 59, 999999),), + (arrow.Arrow(2014, 1, 1), arrow.Arrow(2014, 12, 31, 23, 59, 59, 999999),), + (arrow.Arrow(2015, 1, 1), arrow.Arrow(2015, 12, 31, 23, 59, 59, 999999),), + (arrow.Arrow(2016, 1, 1), arrow.Arrow(2016, 12, 31, 23, 59, 59, 999999),), + ] def test_quarter(self): @@ -1033,13 +957,10 @@ def test_quarter(self): ) ) - self.assertEqual( - result, - [ - (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 6, 30, 23, 59, 59, 999999)), - ], - ) + assert result == [ + (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 6, 30, 23, 59, 59, 999999)), + ] def test_month(self): @@ -1047,15 +968,12 @@ def test_month(self): arrow.Arrow.span_range("month", datetime(2013, 1, 2), datetime(2013, 4, 15)) ) - self.assertEqual( - result, - [ - (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 1, 31, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 2, 1), arrow.Arrow(2013, 2, 28, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 3, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 4, 30, 23, 59, 59, 999999)), - ], - ) + assert result == [ + (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 1, 31, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 2, 1), arrow.Arrow(2013, 2, 28, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 3, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 4, 30, 23, 59, 59, 999999)), + ] def test_week(self): @@ -1063,22 +981,13 @@ def test_week(self): arrow.Arrow.span_range("week", datetime(2013, 2, 2), datetime(2013, 2, 28)) ) - self.assertEqual( - result, - [ - (arrow.Arrow(2013, 1, 28), arrow.Arrow(2013, 2, 3, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 2, 4), arrow.Arrow(2013, 2, 10, 23, 59, 59, 999999)), - ( - arrow.Arrow(2013, 2, 11), - arrow.Arrow(2013, 2, 17, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 2, 18), - arrow.Arrow(2013, 2, 24, 23, 59, 59, 999999), - ), - (arrow.Arrow(2013, 2, 25), arrow.Arrow(2013, 3, 3, 23, 59, 59, 999999)), - ], - ) + assert result == [ + (arrow.Arrow(2013, 1, 28), arrow.Arrow(2013, 2, 3, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 2, 4), arrow.Arrow(2013, 2, 10, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 2, 11), arrow.Arrow(2013, 2, 17, 23, 59, 59, 999999),), + (arrow.Arrow(2013, 2, 18), arrow.Arrow(2013, 2, 24, 23, 59, 59, 999999),), + (arrow.Arrow(2013, 2, 25), arrow.Arrow(2013, 3, 3, 23, 59, 59, 999999)), + ] def test_day(self): @@ -1088,27 +997,12 @@ def test_day(self): ) ) - self.assertEqual( - result, - [ - ( - arrow.Arrow(2013, 1, 1, 0), - arrow.Arrow(2013, 1, 1, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 2, 0), - arrow.Arrow(2013, 1, 2, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 3, 0), - arrow.Arrow(2013, 1, 3, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 4, 0), - arrow.Arrow(2013, 1, 4, 23, 59, 59, 999999), - ), - ], - ) + assert result == [ + (arrow.Arrow(2013, 1, 1, 0), arrow.Arrow(2013, 1, 1, 23, 59, 59, 999999),), + (arrow.Arrow(2013, 1, 2, 0), arrow.Arrow(2013, 1, 2, 23, 59, 59, 999999),), + (arrow.Arrow(2013, 1, 3, 0), arrow.Arrow(2013, 1, 3, 23, 59, 59, 999999),), + (arrow.Arrow(2013, 1, 4, 0), arrow.Arrow(2013, 1, 4, 23, 59, 59, 999999),), + ] def test_days(self): @@ -1118,27 +1012,12 @@ def test_days(self): ) ) - self.assertEqual( - result, - [ - ( - arrow.Arrow(2013, 1, 1, 0), - arrow.Arrow(2013, 1, 1, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 2, 0), - arrow.Arrow(2013, 1, 2, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 3, 0), - arrow.Arrow(2013, 1, 3, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 4, 0), - arrow.Arrow(2013, 1, 4, 23, 59, 59, 999999), - ), - ], - ) + assert result == [ + (arrow.Arrow(2013, 1, 1, 0), arrow.Arrow(2013, 1, 1, 23, 59, 59, 999999),), + (arrow.Arrow(2013, 1, 2, 0), arrow.Arrow(2013, 1, 2, 23, 59, 59, 999999),), + (arrow.Arrow(2013, 1, 3, 0), arrow.Arrow(2013, 1, 3, 23, 59, 59, 999999),), + (arrow.Arrow(2013, 1, 4, 0), arrow.Arrow(2013, 1, 4, 23, 59, 59, 999999),), + ] def test_hour(self): @@ -1148,27 +1027,12 @@ def test_hour(self): ) ) - self.assertEqual( - result, - [ - ( - arrow.Arrow(2013, 1, 1, 0), - arrow.Arrow(2013, 1, 1, 0, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 1), - arrow.Arrow(2013, 1, 1, 1, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 2), - arrow.Arrow(2013, 1, 1, 2, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 3), - arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999), - ), - ], - ) + assert result == [ + (arrow.Arrow(2013, 1, 1, 0), arrow.Arrow(2013, 1, 1, 0, 59, 59, 999999),), + (arrow.Arrow(2013, 1, 1, 1), arrow.Arrow(2013, 1, 1, 1, 59, 59, 999999),), + (arrow.Arrow(2013, 1, 1, 2), arrow.Arrow(2013, 1, 1, 2, 59, 59, 999999),), + (arrow.Arrow(2013, 1, 1, 3), arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999),), + ] result = list( arrow.Arrow.span_range( @@ -1176,10 +1040,9 @@ def test_hour(self): ) ) - self.assertEqual( - result, - [(arrow.Arrow(2013, 1, 1, 3), arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999))], - ) + assert result == [ + (arrow.Arrow(2013, 1, 1, 3), arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999)) + ] def test_minute(self): @@ -1189,27 +1052,12 @@ def test_minute(self): ) ) - self.assertEqual( - result, - [ - ( - arrow.Arrow(2013, 1, 1, 0, 0), - arrow.Arrow(2013, 1, 1, 0, 0, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 1), - arrow.Arrow(2013, 1, 1, 0, 1, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 2), - arrow.Arrow(2013, 1, 1, 0, 2, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 3), - arrow.Arrow(2013, 1, 1, 0, 3, 59, 999999), - ), - ], - ) + assert result == [ + (arrow.Arrow(2013, 1, 1, 0, 0), arrow.Arrow(2013, 1, 1, 0, 0, 59, 999999),), + (arrow.Arrow(2013, 1, 1, 0, 1), arrow.Arrow(2013, 1, 1, 0, 1, 59, 999999),), + (arrow.Arrow(2013, 1, 1, 0, 2), arrow.Arrow(2013, 1, 1, 0, 2, 59, 999999),), + (arrow.Arrow(2013, 1, 1, 0, 3), arrow.Arrow(2013, 1, 1, 0, 3, 59, 999999),), + ] def test_second(self): @@ -1219,27 +1067,24 @@ def test_second(self): ) ) - self.assertEqual( - result, - [ - ( - arrow.Arrow(2013, 1, 1, 0, 0, 0), - arrow.Arrow(2013, 1, 1, 0, 0, 0, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 0, 1), - arrow.Arrow(2013, 1, 1, 0, 0, 1, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 0, 2), - arrow.Arrow(2013, 1, 1, 0, 0, 2, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 0, 3), - arrow.Arrow(2013, 1, 1, 0, 0, 3, 999999), - ), - ], - ) + assert result == [ + ( + arrow.Arrow(2013, 1, 1, 0, 0, 0), + arrow.Arrow(2013, 1, 1, 0, 0, 0, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 0, 0, 1), + arrow.Arrow(2013, 1, 1, 0, 0, 1, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 0, 0, 2), + arrow.Arrow(2013, 1, 1, 0, 0, 2, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 0, 0, 3), + arrow.Arrow(2013, 1, 1, 0, 0, 3, 999999), + ), + ] def test_naive_tz(self): @@ -1250,8 +1095,8 @@ def test_naive_tz(self): ) for f, c in result: - self.assertEqual(f.tzinfo, tzinfo) - self.assertEqual(c.tzinfo, tzinfo) + assert f.tzinfo == tzinfo + assert c.tzinfo == tzinfo def test_aware_same_tz(self): @@ -1264,8 +1109,8 @@ def test_aware_same_tz(self): ) for f, c in result: - self.assertEqual(f.tzinfo, tzinfo) - self.assertEqual(c.tzinfo, tzinfo) + assert f.tzinfo == tzinfo + assert c.tzinfo == tzinfo def test_aware_different_tz(self): @@ -1279,8 +1124,8 @@ def test_aware_different_tz(self): ) for f, c in result: - self.assertEqual(f.tzinfo, tzinfo1) - self.assertEqual(c.tzinfo, tzinfo1) + assert f.tzinfo == tzinfo1 + assert c.tzinfo == tzinfo1 def test_aware_tz(self): @@ -1292,8 +1137,8 @@ def test_aware_tz(self): ) for f, c in result: - self.assertEqual(f.tzinfo, tz.gettz("US/Central")) - self.assertEqual(c.tzinfo, tz.gettz("US/Central")) + assert f.tzinfo == tz.gettz("US/Central") + assert c.tzinfo == tz.gettz("US/Central") def test_bounds_param_is_passed(self): @@ -1303,28 +1148,20 @@ def test_bounds_param_is_passed(self): ) ) - self.assertEqual( - result, - [ - (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 4, 1)), - (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 7, 1)), - ], - ) + assert result == [ + (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 4, 1)), + (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 7, 1)), + ] -class ArrowIntervalTests(Chai): +class TestArrowInterval: def test_incorrect_input(self): - correct = True - try: + with pytest.raises(ValueError): list( arrow.Arrow.interval( "month", datetime(2013, 1, 2), datetime(2013, 4, 15), 0 ) ) - except: # noqa: E722 - correct = False - - self.assertEqual(correct, False) def test_correct(self): result = list( @@ -1333,23 +1170,11 @@ def test_correct(self): ) ) - self.assertEqual( - result, - [ - ( - arrow.Arrow(2013, 5, 5, 12), - arrow.Arrow(2013, 5, 5, 13, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 5, 5, 14), - arrow.Arrow(2013, 5, 5, 15, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 5, 5, 16), - arrow.Arrow(2013, 5, 5, 17, 59, 59, 999999), - ), - ], - ) + assert result == [ + (arrow.Arrow(2013, 5, 5, 12), arrow.Arrow(2013, 5, 5, 13, 59, 59, 999999),), + (arrow.Arrow(2013, 5, 5, 14), arrow.Arrow(2013, 5, 5, 15, 59, 59, 999999),), + (arrow.Arrow(2013, 5, 5, 16), arrow.Arrow(2013, 5, 5, 17, 59, 59, 999999),), + ] def test_bounds_param_is_passed(self): result = list( @@ -1362,355 +1187,288 @@ def test_bounds_param_is_passed(self): ) ) - self.assertEqual( - result, - [ - (arrow.Arrow(2013, 5, 5, 12), arrow.Arrow(2013, 5, 5, 14)), - (arrow.Arrow(2013, 5, 5, 14), arrow.Arrow(2013, 5, 5, 16)), - (arrow.Arrow(2013, 5, 5, 16), arrow.Arrow(2013, 5, 5, 18)), - ], - ) + assert result == [ + (arrow.Arrow(2013, 5, 5, 12), arrow.Arrow(2013, 5, 5, 14)), + (arrow.Arrow(2013, 5, 5, 14), arrow.Arrow(2013, 5, 5, 16)), + (arrow.Arrow(2013, 5, 5, 16), arrow.Arrow(2013, 5, 5, 18)), + ] -class ArrowSpanTests(Chai): - def setUp(self): - super(ArrowSpanTests, self).setUp() - - self.datetime = datetime(2013, 2, 15, 3, 41, 22, 8923) - self.arrow = arrow.Arrow.fromdatetime(self.datetime) +class TestArrowSpan: + @classmethod + def setup_class(cls): + cls.datetime = datetime(2013, 2, 15, 3, 41, 22, 8923) + cls.arrow = arrow.Arrow.fromdatetime(cls.datetime) def test_span_attribute(self): - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): self.arrow.span("span") def test_span_year(self): floor, ceil = self.arrow.span("year") - self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - ) + 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") - self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 3, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - ) + 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) - self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 6, 30, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - ) + 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) - self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2014, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - ) + 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") - self.assertEqual(floor, datetime(2013, 2, 1, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 2, 28, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - ) + assert floor == datetime(2013, 2, 1, tzinfo=tz.tzutc()) + assert ceil == datetime(2013, 2, 28, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_week(self): floor, ceil = self.arrow.span("week") - self.assertEqual(floor, datetime(2013, 2, 11, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 2, 17, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - ) + assert floor == datetime(2013, 2, 11, tzinfo=tz.tzutc()) + assert ceil == datetime(2013, 2, 17, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_day(self): floor, ceil = self.arrow.span("day") - self.assertEqual(floor, datetime(2013, 2, 15, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 2, 15, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - ) + 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") - self.assertEqual(floor, datetime(2013, 2, 15, 3, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 2, 15, 3, 59, 59, 999999, tzinfo=tz.tzutc()) - ) + 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") - self.assertEqual(floor, datetime(2013, 2, 15, 3, 41, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 2, 15, 3, 41, 59, 999999, tzinfo=tz.tzutc()) - ) + 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") - self.assertEqual(floor, datetime(2013, 2, 15, 3, 41, 22, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 2, 15, 3, 41, 22, 999999, tzinfo=tz.tzutc()) - ) + 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") - self.assertEqual( - floor, datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) - ) - self.assertEqual( - ceil, datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) - ) + 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") - self.assertEqual(floor, self.arrow.floor("month")) - self.assertEqual(ceil, self.arrow.ceil("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="[]") - self.assertEqual(floor, datetime(2013, 2, 15, 3, tzinfo=tz.tzutc())) - self.assertEqual(ceil, datetime(2013, 2, 15, 4, tzinfo=tz.tzutc())) + 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="(]") - self.assertEqual(floor, datetime(2013, 2, 15, 3, 0, 0, 1, tzinfo=tz.tzutc())) - self.assertEqual(ceil, datetime(2013, 2, 15, 4, tzinfo=tz.tzutc())) + 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="()") - self.assertEqual(floor, datetime(2013, 2, 15, 3, 0, 0, 1, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 2, 15, 3, 59, 59, 999999, tzinfo=tz.tzutc()) - ) + 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 self.assertRaises(AttributeError): + with pytest.raises(AttributeError): floor, ceil = self.arrow.span("hour", bounds="][") -class ArrowHumanizeTests(Chai): - def setUp(self): - super(ArrowHumanizeTests, self).setUp() - - self.datetime = datetime(2013, 1, 1) - self.now = arrow.Arrow.utcnow() +class TestArrowHumanize: + @classmethod + def setup_class(cls): + cls.datetime = datetime(2013, 1, 1) + cls.now = arrow.Arrow.utcnow() def test_granularity(self): - self.assertEqual(self.now.humanize(granularity="second"), "just now") + assert self.now.humanize(granularity="second") == "just now" later1 = self.now.shift(seconds=1) - self.assertEqual(self.now.humanize(later1, granularity="second"), "just now") - self.assertEqual(later1.humanize(self.now, granularity="second"), "just now") - self.assertEqual( - self.now.humanize(later1, granularity="minute"), "0 minutes ago" - ) - self.assertEqual( - later1.humanize(self.now, granularity="minute"), "in 0 minutes" - ) + assert self.now.humanize(later1, granularity="second") == "just now" + assert later1.humanize(self.now, granularity="second") == "just now" + assert self.now.humanize(later1, granularity="minute") == "0 minutes ago" + assert later1.humanize(self.now, granularity="minute") == "in 0 minutes" later100 = self.now.shift(seconds=100) - self.assertEqual( - self.now.humanize(later100, granularity="second"), "100 seconds ago" - ) - self.assertEqual( - later100.humanize(self.now, granularity="second"), "in 100 seconds" - ) - self.assertEqual( - self.now.humanize(later100, granularity="minute"), "a minute ago" - ) - self.assertEqual( - later100.humanize(self.now, granularity="minute"), "in a minute" - ) - self.assertEqual(self.now.humanize(later100, granularity="hour"), "0 hours ago") - self.assertEqual(later100.humanize(self.now, granularity="hour"), "in 0 hours") + assert self.now.humanize(later100, granularity="second") == "100 seconds ago" + assert later100.humanize(self.now, granularity="second") == "in 100 seconds" + assert self.now.humanize(later100, granularity="minute") == "a minute ago" + assert later100.humanize(self.now, granularity="minute") == "in a minute" + assert self.now.humanize(later100, granularity="hour") == "0 hours ago" + assert later100.humanize(self.now, granularity="hour") == "in 0 hours" later4000 = self.now.shift(seconds=4000) - self.assertEqual( - self.now.humanize(later4000, granularity="minute"), "66 minutes ago" - ) - self.assertEqual( - later4000.humanize(self.now, granularity="minute"), "in 66 minutes" - ) - self.assertEqual( - self.now.humanize(later4000, granularity="hour"), "an hour ago" - ) - self.assertEqual(later4000.humanize(self.now, granularity="hour"), "in an hour") - self.assertEqual(self.now.humanize(later4000, granularity="day"), "0 days ago") - self.assertEqual(later4000.humanize(self.now, granularity="day"), "in 0 days") + assert self.now.humanize(later4000, granularity="minute") == "66 minutes ago" + assert later4000.humanize(self.now, granularity="minute") == "in 66 minutes" + assert self.now.humanize(later4000, granularity="hour") == "an hour ago" + assert later4000.humanize(self.now, granularity="hour") == "in an hour" + 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) - self.assertEqual( - self.now.humanize(later105, granularity="hour"), "27 hours ago" - ) - self.assertEqual(later105.humanize(self.now, granularity="hour"), "in 27 hours") - self.assertEqual(self.now.humanize(later105, granularity="day"), "a day ago") - self.assertEqual(later105.humanize(self.now, granularity="day"), "in a day") - self.assertEqual(self.now.humanize(later105, granularity="week"), "0 weeks ago") - self.assertEqual(later105.humanize(self.now, granularity="week"), "in 0 weeks") - self.assertEqual( - self.now.humanize(later105, granularity="month"), "0 months ago" - ) - self.assertEqual( - later105.humanize(self.now, granularity="month"), "in 0 months" - ) - self.assertEqual( - self.now.humanize(later105, granularity=["month"]), "0 months ago" - ) - self.assertEqual( - later105.humanize(self.now, granularity=["month"]), "in 0 months" - ) + 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" + assert later105.humanize(self.now, granularity="day") == "in a day" + assert self.now.humanize(later105, granularity="week") == "0 weeks ago" + assert later105.humanize(self.now, granularity="week") == "in 0 weeks" + assert self.now.humanize(later105, granularity="month") == "0 months ago" + assert later105.humanize(self.now, granularity="month") == "in 0 months" + 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) - self.assertEqual(self.now.humanize(later106, granularity="day"), "34 days ago") - self.assertEqual(later106.humanize(self.now, granularity="day"), "in 34 days") - self.assertEqual(self.now.humanize(later106, granularity="week"), "4 weeks ago") - self.assertEqual(later106.humanize(self.now, granularity="week"), "in 4 weeks") - self.assertEqual( - self.now.humanize(later106, granularity="month"), "a month ago" - ) - self.assertEqual(later106.humanize(self.now, granularity="month"), "in a month") - self.assertEqual(self.now.humanize(later106, granularity="year"), "0 years ago") - self.assertEqual(later106.humanize(self.now, granularity="year"), "in 0 years") + 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" + assert later106.humanize(self.now, granularity="week") == "in 4 weeks" + assert self.now.humanize(later106, granularity="month") == "a month ago" + assert later106.humanize(self.now, granularity="month") == "in a month" + 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) - self.assertEqual( - self.now.humanize(later506, granularity="week"), "82 weeks ago" - ) - self.assertEqual(later506.humanize(self.now, granularity="week"), "in 82 weeks") - self.assertEqual( - self.now.humanize(later506, granularity="month"), "18 months ago" - ) - self.assertEqual( - later506.humanize(self.now, granularity="month"), "in 18 months" - ) - self.assertEqual(self.now.humanize(later506, granularity="year"), "a year ago") - self.assertEqual(later506.humanize(self.now, granularity="year"), "in a year") + 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" + assert later506.humanize(self.now, granularity="month") == "in 18 months" + assert self.now.humanize(later506, granularity="year") == "a year ago" + assert later506.humanize(self.now, granularity="year") == "in a year" later108 = self.now.shift(seconds=10 ** 8) - self.assertEqual(self.now.humanize(later108, granularity="year"), "3 years ago") - self.assertEqual(later108.humanize(self.now, granularity="year"), "in 3 years") + 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) - self.assertEqual( + assert ( self.now.humanize( later108onlydistance, only_distance=True, granularity="year" - ), - "3 years", + ) + == "3 years" ) - self.assertEqual( + assert ( later108onlydistance.humanize( self.now, only_distance=True, granularity="year" - ), - "3 years", + ) + == "3 years" ) - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): self.now.humanize(later108, granularity="years") def test_multiple_granularity(self): - self.assertEqual(self.now.humanize(granularity="second"), "just now") - self.assertEqual(self.now.humanize(granularity=["second"]), "just now") - self.assertEqual( - self.now.humanize(granularity=["year", "month", "day", "hour", "second"]), - "in 0 years 0 months 0 days 0 hours and 0 seconds", + assert self.now.humanize(granularity="second") == "just now" + assert self.now.humanize(granularity=["second"]) == "just now" + assert ( + self.now.humanize(granularity=["year", "month", "day", "hour", "second"]) + == "in 0 years 0 months 0 days 0 hours and 0 seconds" ) later4000 = self.now.shift(seconds=4000) - self.assertEqual( - later4000.humanize(self.now, granularity=["hour", "minute"]), - "in an hour and 6 minutes", + assert ( + later4000.humanize(self.now, granularity=["hour", "minute"]) + == "in an hour and 6 minutes" ) - self.assertEqual( - self.now.humanize(later4000, granularity=["hour", "minute"]), - "an hour and 6 minutes ago", + assert ( + self.now.humanize(later4000, granularity=["hour", "minute"]) + == "an hour and 6 minutes ago" ) - self.assertEqual( + assert ( later4000.humanize( self.now, granularity=["hour", "minute"], only_distance=True - ), - "an hour and 6 minutes", + ) + == "an hour and 6 minutes" ) - self.assertEqual( - later4000.humanize(self.now, granularity=["day", "hour", "minute"]), - "in 0 days an hour and 6 minutes", + assert ( + later4000.humanize(self.now, granularity=["day", "hour", "minute"]) + == "in 0 days an hour and 6 minutes" ) - self.assertEqual( - self.now.humanize(later4000, granularity=["day", "hour", "minute"]), - "0 days an hour and 6 minutes ago", + assert ( + self.now.humanize(later4000, granularity=["day", "hour", "minute"]) + == "0 days an hour and 6 minutes ago" ) later105 = self.now.shift(seconds=10 ** 5) - self.assertEqual( - self.now.humanize(later105, granularity=["hour", "day", "minute"]), - "a day 3 hours and 46 minutes ago", + assert ( + self.now.humanize(later105, granularity=["hour", "day", "minute"]) + == "a day 3 hours and 46 minutes ago" ) - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): self.now.humanize(later105, granularity=["error", "second"]) later108onlydistance = self.now.shift(seconds=10 ** 8) - self.assertEqual( + assert ( self.now.humanize( later108onlydistance, only_distance=True, granularity=["year"] - ), - "3 years", + ) + == "3 years" ) - self.assertEqual( + assert ( self.now.humanize( later108onlydistance, only_distance=True, granularity=["month", "week"] - ), - "37 months and 4 weeks", + ) + == "37 months and 4 weeks" ) - self.assertEqual( + assert ( self.now.humanize( later108onlydistance, only_distance=True, granularity=["year", "second"] - ), - "3 years and 5327200 seconds", + ) + == "3 years and 5327200 seconds" ) one_min_one_sec_ago = self.now.shift(minutes=-1, seconds=-1) - self.assertEqual( - one_min_one_sec_ago.humanize(self.now, granularity=["minute", "second"]), - "a minute and a second ago", + assert ( + one_min_one_sec_ago.humanize(self.now, granularity=["minute", "second"]) + == "a minute and a second ago" ) one_min_two_secs_ago = self.now.shift(minutes=-1, seconds=-2) - self.assertEqual( - one_min_two_secs_ago.humanize(self.now, granularity=["minute", "second"]), - "a minute and 2 seconds ago", + assert ( + one_min_two_secs_ago.humanize(self.now, granularity=["minute", "second"]) + == "a minute and 2 seconds ago" ) def test_seconds(self): @@ -1718,165 +1476,165 @@ def test_seconds(self): later = self.now.shift(seconds=10) # regression test for issue #727 - self.assertEqual(self.now.humanize(later), "10 seconds ago") - self.assertEqual(later.humanize(self.now), "in 10 seconds") + assert self.now.humanize(later) == "10 seconds ago" + assert later.humanize(self.now) == "in 10 seconds" - self.assertEqual(self.now.humanize(later, only_distance=True), "10 seconds") - self.assertEqual(later.humanize(self.now, only_distance=True), "10 seconds") + assert self.now.humanize(later, only_distance=True) == "10 seconds" + assert later.humanize(self.now, only_distance=True) == "10 seconds" def test_minute(self): later = self.now.shift(minutes=1) - self.assertEqual(self.now.humanize(later), "a minute ago") - self.assertEqual(later.humanize(self.now), "in a minute") + assert self.now.humanize(later) == "a minute ago" + assert later.humanize(self.now) == "in a minute" - self.assertEqual(self.now.humanize(later, only_distance=True), "a minute") - self.assertEqual(later.humanize(self.now, only_distance=True), "a minute") + assert self.now.humanize(later, only_distance=True) == "a minute" + assert later.humanize(self.now, only_distance=True) == "a minute" def test_minutes(self): later = self.now.shift(minutes=2) - self.assertEqual(self.now.humanize(later), "2 minutes ago") - self.assertEqual(later.humanize(self.now), "in 2 minutes") + assert self.now.humanize(later) == "2 minutes ago" + assert later.humanize(self.now) == "in 2 minutes" - self.assertEqual(self.now.humanize(later, only_distance=True), "2 minutes") - self.assertEqual(later.humanize(self.now, only_distance=True), "2 minutes") + assert self.now.humanize(later, only_distance=True) == "2 minutes" + assert later.humanize(self.now, only_distance=True) == "2 minutes" def test_hour(self): later = self.now.shift(hours=1) - self.assertEqual(self.now.humanize(later), "an hour ago") - self.assertEqual(later.humanize(self.now), "in an hour") + assert self.now.humanize(later) == "an hour ago" + assert later.humanize(self.now) == "in an hour" - self.assertEqual(self.now.humanize(later, only_distance=True), "an hour") - self.assertEqual(later.humanize(self.now, only_distance=True), "an hour") + assert self.now.humanize(later, only_distance=True) == "an hour" + assert later.humanize(self.now, only_distance=True) == "an hour" def test_hours(self): later = self.now.shift(hours=2) - self.assertEqual(self.now.humanize(later), "2 hours ago") - self.assertEqual(later.humanize(self.now), "in 2 hours") + assert self.now.humanize(later) == "2 hours ago" + assert later.humanize(self.now) == "in 2 hours" - self.assertEqual(self.now.humanize(later, only_distance=True), "2 hours") - self.assertEqual(later.humanize(self.now, only_distance=True), "2 hours") + assert self.now.humanize(later, only_distance=True) == "2 hours" + assert later.humanize(self.now, only_distance=True) == "2 hours" def test_day(self): later = self.now.shift(days=1) - self.assertEqual(self.now.humanize(later), "a day ago") - self.assertEqual(later.humanize(self.now), "in a day") + assert self.now.humanize(later) == "a day ago" + assert later.humanize(self.now) == "in a day" # regression test for issue #697 less_than_48_hours = self.now.shift( days=1, hours=23, seconds=59, microseconds=999999 ) - self.assertEqual(self.now.humanize(less_than_48_hours), "a day ago") - self.assertEqual(less_than_48_hours.humanize(self.now), "in a day") + assert self.now.humanize(less_than_48_hours) == "a day ago" + assert less_than_48_hours.humanize(self.now) == "in a day" less_than_48_hours_date = less_than_48_hours._datetime.date() - with self.assertRaises(TypeError): + with pytest.raises(TypeError): # humanize other argument does not take raw datetime.date objects self.now.humanize(less_than_48_hours_date) # convert from date to arrow object less_than_48_hours_date = arrow.Arrow.fromdate(less_than_48_hours_date) - self.assertEqual(self.now.humanize(less_than_48_hours_date), "a day ago") - self.assertEqual(less_than_48_hours_date.humanize(self.now), "in a day") + assert self.now.humanize(less_than_48_hours_date) == "a day ago" + assert less_than_48_hours_date.humanize(self.now) == "in a day" - self.assertEqual(self.now.humanize(later, only_distance=True), "a day") - self.assertEqual(later.humanize(self.now, only_distance=True), "a day") + assert self.now.humanize(later, only_distance=True) == "a day" + assert later.humanize(self.now, only_distance=True) == "a day" def test_days(self): later = self.now.shift(days=2) - self.assertEqual(self.now.humanize(later), "2 days ago") - self.assertEqual(later.humanize(self.now), "in 2 days") + assert self.now.humanize(later) == "2 days ago" + assert later.humanize(self.now) == "in 2 days" - self.assertEqual(self.now.humanize(later, only_distance=True), "2 days") - self.assertEqual(later.humanize(self.now, only_distance=True), "2 days") + assert self.now.humanize(later, only_distance=True) == "2 days" + assert later.humanize(self.now, only_distance=True) == "2 days" # Regression tests for humanize bug referenced in issue 541 later = self.now.shift(days=3) - self.assertEqual(later.humanize(), "in 3 days") + assert later.humanize() == "in 3 days" later = self.now.shift(days=3, seconds=1) - self.assertEqual(later.humanize(), "in 3 days") + assert later.humanize() == "in 3 days" later = self.now.shift(days=4) - self.assertEqual(later.humanize(), "in 4 days") + assert later.humanize() == "in 4 days" def test_week(self): later = self.now.shift(weeks=1) - self.assertEqual(self.now.humanize(later), "a week ago") - self.assertEqual(later.humanize(self.now), "in a week") + assert self.now.humanize(later) == "a week ago" + assert later.humanize(self.now) == "in a week" - self.assertEqual(self.now.humanize(later, only_distance=True), "a week") - self.assertEqual(later.humanize(self.now, only_distance=True), "a week") + assert self.now.humanize(later, only_distance=True) == "a week" + assert later.humanize(self.now, only_distance=True) == "a week" def test_weeks(self): later = self.now.shift(weeks=2) - self.assertEqual(self.now.humanize(later), "2 weeks ago") - self.assertEqual(later.humanize(self.now), "in 2 weeks") + assert self.now.humanize(later) == "2 weeks ago" + assert later.humanize(self.now) == "in 2 weeks" - self.assertEqual(self.now.humanize(later, only_distance=True), "2 weeks") - self.assertEqual(later.humanize(self.now, only_distance=True), "2 weeks") + assert self.now.humanize(later, only_distance=True) == "2 weeks" + assert later.humanize(self.now, only_distance=True) == "2 weeks" def test_month(self): later = self.now.shift(months=1) - self.assertEqual(self.now.humanize(later), "a month ago") - self.assertEqual(later.humanize(self.now), "in a month") + assert self.now.humanize(later) == "a month ago" + assert later.humanize(self.now) == "in a month" - self.assertEqual(self.now.humanize(later, only_distance=True), "a month") - self.assertEqual(later.humanize(self.now, only_distance=True), "a month") + assert self.now.humanize(later, only_distance=True) == "a month" + assert later.humanize(self.now, only_distance=True) == "a month" def test_months(self): later = self.now.shift(months=2) earlier = self.now.shift(months=-2) - self.assertEqual(earlier.humanize(self.now), "2 months ago") - self.assertEqual(later.humanize(self.now), "in 2 months") + assert earlier.humanize(self.now) == "2 months ago" + assert later.humanize(self.now) == "in 2 months" - self.assertEqual(self.now.humanize(later, only_distance=True), "2 months") - self.assertEqual(later.humanize(self.now, only_distance=True), "2 months") + assert self.now.humanize(later, only_distance=True) == "2 months" + assert later.humanize(self.now, only_distance=True) == "2 months" def test_year(self): later = self.now.shift(years=1) - self.assertEqual(self.now.humanize(later), "a year ago") - self.assertEqual(later.humanize(self.now), "in a year") + assert self.now.humanize(later) == "a year ago" + assert later.humanize(self.now) == "in a year" - self.assertEqual(self.now.humanize(later, only_distance=True), "a year") - self.assertEqual(later.humanize(self.now, only_distance=True), "a year") + assert self.now.humanize(later, only_distance=True) == "a year" + assert later.humanize(self.now, only_distance=True) == "a year" def test_years(self): later = self.now.shift(years=2) - self.assertEqual(self.now.humanize(later), "2 years ago") - self.assertEqual(later.humanize(self.now), "in 2 years") + assert self.now.humanize(later) == "2 years ago" + assert later.humanize(self.now) == "in 2 years" - self.assertEqual(self.now.humanize(later, only_distance=True), "2 years") - self.assertEqual(later.humanize(self.now, only_distance=True), "2 years") + assert self.now.humanize(later, only_distance=True) == "2 years" + assert later.humanize(self.now, only_distance=True) == "2 years" arw = arrow.Arrow(2014, 7, 2) result = arw.humanize(self.datetime) - self.assertEqual(result, "in 2 years") + assert result == "in 2 years" def test_arrow(self): @@ -1884,7 +1642,7 @@ def test_arrow(self): result = arw.humanize(arrow.Arrow.fromdatetime(self.datetime)) - self.assertEqual(result, "just now") + assert result == "just now" def test_datetime_tzinfo(self): @@ -1892,20 +1650,20 @@ def test_datetime_tzinfo(self): result = arw.humanize(self.datetime.replace(tzinfo=tz.tzutc())) - self.assertEqual(result, "just now") + assert result == "just now" def test_other(self): arw = arrow.Arrow.fromdatetime(self.datetime) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): arw.humanize(object()) def test_invalid_locale(self): arw = arrow.Arrow.fromdatetime(self.datetime) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): arw.humanize(locale="klingon") def test_none(self): @@ -1914,11 +1672,11 @@ def test_none(self): result = arw.humanize() - self.assertEqual(result, "just now") + assert result == "just now" result = arw.humanize(None) - self.assertEqual(result, "just now") + assert result == "just now" def test_untranslated_granularity(self): @@ -1929,15 +1687,14 @@ def test_untranslated_granularity(self): with patch.dict("arrow.locales.EnglishLocale.timeframes"): del arrow.locales.EnglishLocale.timeframes["week"] - with self.assertRaises(ValueError): + with pytest.raises(ValueError): arw.humanize(later, granularity="week") -class ArrowHumanizeTestsWithLocale(Chai): - def setUp(self): - super(ArrowHumanizeTestsWithLocale, self).setUp() - - self.datetime = datetime(2013, 1, 1) +class TestArrowHumanizeTestsWithLocale: + @classmethod + def setup_class(cls): + cls.datetime = datetime(2013, 1, 1) def test_now(self): @@ -1945,14 +1702,14 @@ def test_now(self): result = arw.humanize(self.datetime, locale="ru") - self.assertEqual(result, "сейчас") + assert result == "сейчас" def test_seconds(self): arw = arrow.Arrow(2013, 1, 1, 0, 0, 44) result = arw.humanize(self.datetime, locale="ru") - self.assertEqual(result, "через 44 несколько секунд") + assert result == "через 44 несколько секунд" def test_years(self): @@ -1960,87 +1717,87 @@ def test_years(self): result = arw.humanize(self.datetime, locale="ru") - self.assertEqual(result, "2 года назад") + assert result == "2 года назад" -class ArrowIsBetweenTests(Chai): +class TestArrowIsBetween: def test_start_before_end(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) result = target.is_between(start, end) - self.assertFalse(result) + assert not result def test_exclusive_exclusive_bounds(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 27)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 10)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 36)) result = target.is_between(start, end, "()") - self.assertTrue(result) + assert result result = target.is_between(start, end) - self.assertTrue(result) + assert result def test_exclusive_exclusive_bounds_same_date(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) result = target.is_between(start, end, "()") - self.assertFalse(result) + assert not result def test_inclusive_exclusive_bounds(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 6)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 4)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 6)) result = target.is_between(start, end, "[)") - self.assertFalse(result) + assert not result def test_exclusive_inclusive_bounds(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) result = target.is_between(start, end, "(]") - self.assertTrue(result) + assert result def test_inclusive_inclusive_bounds_same_date(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) result = target.is_between(start, end, "[]") - self.assertTrue(result) + assert result def test_type_error_exception(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = datetime(2013, 5, 5) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) target.is_between(start, end) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) end = datetime(2013, 5, 8) target.is_between(start, end) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): target.is_between(None, None) def test_attribute_error_exception(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): target.is_between(start, end, "][") - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): target.is_between(start, end, "") - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): target.is_between(start, end, "]") - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): target.is_between(start, end, "[") - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): target.is_between(start, end, "hello") -class ArrowUtilTests(Chai): +class TestArrowUtil: def test_get_datetime(self): get_datetime = arrow.Arrow._get_datetime @@ -2049,33 +1806,29 @@ def test_get_datetime(self): dt = datetime.utcnow() timestamp = time.time() - self.assertEqual(get_datetime(arw), arw.datetime) - self.assertEqual(get_datetime(dt), dt) - self.assertEqual( - get_datetime(timestamp), arrow.Arrow.utcfromtimestamp(timestamp).datetime + assert get_datetime(arw) == arw.datetime + assert get_datetime(dt) == dt + assert ( + get_datetime(timestamp) == arrow.Arrow.utcfromtimestamp(timestamp).datetime ) - with self.assertRaises(ValueError) as raise_ctx: + with pytest.raises(ValueError) as raise_ctx: get_datetime("abc") - self.assertFalse("{}" in str(raise_ctx.exception)) + 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 self.assertRaises(ValueError) as raise_ctx: + with pytest.raises(ValueError) as raise_ctx: get_tzinfo("abc") - self.assertFalse("{}" in str(raise_ctx.exception)) + assert "not recognized as a timezone" in str(raise_ctx.value) def test_get_iteration_params(self): - self.assertEqual( - arrow.Arrow._get_iteration_params("end", None), ("end", sys.maxsize) - ) - self.assertEqual( - arrow.Arrow._get_iteration_params(None, 100), (arrow.Arrow.max, 100) - ) - self.assertEqual(arrow.Arrow._get_iteration_params(100, 120), (100, 120)) + 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) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): arrow.Arrow._get_iteration_params(None, None) diff --git a/tests/arrow_tests.py.bak b/tests/arrow_tests.py.bak new file mode 100644 index 000000000..9d6827660 --- /dev/null +++ b/tests/arrow_tests.py.bak @@ -0,0 +1,2081 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import calendar +import pickle +import sys +import time +from datetime import date, datetime, timedelta + +import pytz +import simplejson as json +from chai import Chai +from dateutil import tz +from dateutil.relativedelta import FR, MO, SA, SU, TH, TU, WE +from mock import patch + +from arrow import arrow, util + + +def assertDtEqual(dt1, dt2, within=10): + assertEqual(dt1.tzinfo, dt2.tzinfo) # noqa: F821 + assertTrue(abs(util.total_seconds(dt1 - dt2)) < within) # noqa: F821 + + +class ArrowInitTests(Chai): + def test_init_bad_input(self): + + with self.assertRaises(TypeError): + arrow.Arrow(2013) + + with self.assertRaises(TypeError): + arrow.Arrow(2013, 2) + + with self.assertRaises(ValueError): + 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()) + self.assertEqual(result._datetime, self.expected) + + result = arrow.Arrow(2013, 2, 2, 12) + self.expected = datetime(2013, 2, 2, 12, tzinfo=tz.tzutc()) + self.assertEqual(result._datetime, self.expected) + + result = arrow.Arrow(2013, 2, 2, 12, 30) + self.expected = datetime(2013, 2, 2, 12, 30, tzinfo=tz.tzutc()) + self.assertEqual(result._datetime, self.expected) + + result = arrow.Arrow(2013, 2, 2, 12, 30, 45) + self.expected = datetime(2013, 2, 2, 12, 30, 45, tzinfo=tz.tzutc()) + self.assertEqual(result._datetime, self.expected) + + result = arrow.Arrow(2013, 2, 2, 12, 30, 45, 999999) + self.expected = datetime(2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.tzutc()) + self.assertEqual(result._datetime, self.expected) + + result = arrow.Arrow( + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") + ) + self.expected = datetime( + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") + ) + self.assertEqual(result._datetime, self.expected) + + # 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") + ) + self.expected = datetime( + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") + ) + self.assertEqual(result._datetime, self.expected) + assertDtEqual(result._datetime, self.expected, 1) + + +class ArrowFactoryTests(Chai): + def test_now(self): + + result = arrow.Arrow.now() + + assertDtEqual(result._datetime, datetime.now().replace(tzinfo=tz.tzlocal())) + + def test_utcnow(self): + + result = arrow.Arrow.utcnow() + + assertDtEqual(result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc())) + + def test_fromtimestamp(self): + + timestamp = time.time() + + result = arrow.Arrow.fromtimestamp(timestamp) + assertDtEqual(result._datetime, datetime.now().replace(tzinfo=tz.tzlocal())) + + result = arrow.Arrow.fromtimestamp(timestamp, tzinfo=tz.gettz("Europe/Paris")) + assertDtEqual( + result._datetime, + datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), + ) + + result = arrow.Arrow.fromtimestamp(timestamp, tzinfo="Europe/Paris") + assertDtEqual( + result._datetime, + datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), + ) + + with self.assertRaises(ValueError): + arrow.Arrow.fromtimestamp("invalid timestamp") + + def test_utcfromtimestamp(self): + + timestamp = time.time() + + result = arrow.Arrow.utcfromtimestamp(timestamp) + assertDtEqual(result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc())) + + with self.assertRaises(ValueError): + arrow.Arrow.utcfromtimestamp("invalid timestamp") + + def test_fromdatetime(self): + + dt = datetime(2013, 2, 3, 12, 30, 45, 1) + + result = arrow.Arrow.fromdatetime(dt) + + self.assertEqual(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) + + self.assertEqual(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")) + + self.assertEqual(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")) + + self.assertEqual( + 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") + self.assertEqual( + 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") + ) + self.assertEqual( + result._datetime, + datetime(2013, 2, 3, 12, 30, 45, tzinfo=tz.gettz("Europe/Paris")), + ) + + +class ArrowRepresentationTests(Chai): + def setUp(self): + super(ArrowRepresentationTests, self).setUp() + + self.arrow = arrow.Arrow(2013, 2, 3, 12, 30, 45, 1) + + def test_repr(self): + + result = self.arrow.__repr__() + + self.assertEqual( + result, "".format(self.arrow._datetime.isoformat()) + ) + + def test_str(self): + + result = self.arrow.__str__() + + self.assertEqual(result, self.arrow._datetime.isoformat()) + + def test_hash(self): + + result = self.arrow.__hash__() + + self.assertEqual(result, self.arrow._datetime.__hash__()) + + def test_format(self): + + result = "{:YYYY-MM-DD}".format(self.arrow) + + self.assertEqual(result, "2013-02-03") + + def test_bare_format(self): + + result = self.arrow.format() + + self.assertEqual(result, "2013-02-03 12:30:45+00:00") + + def test_format_no_format_string(self): + + result = "{}".format(self.arrow) + + self.assertEqual(result, str(self.arrow)) + + def test_clone(self): + + result = self.arrow.clone() + + self.assertTrue(result is not self.arrow) + self.assertEqual(result._datetime, self.arrow._datetime) + + +class ArrowAttributeTests(Chai): + def setUp(self): + super(ArrowAttributeTests, self).setUp() + + self.arrow = arrow.Arrow(2013, 1, 1) + + def test_getattr_base(self): + + with self.assertRaises(AttributeError): + self.arrow.prop + + def test_getattr_week(self): + + self.assertEqual(self.arrow.week, 1) + + def test_getattr_quarter(self): + # start dates + q1 = arrow.Arrow(2013, 1, 1) + q2 = arrow.Arrow(2013, 4, 1) + q3 = arrow.Arrow(2013, 8, 1) + q4 = arrow.Arrow(2013, 10, 1) + self.assertEqual(q1.quarter, 1) + self.assertEqual(q2.quarter, 2) + self.assertEqual(q3.quarter, 3) + self.assertEqual(q4.quarter, 4) + + # end dates + q1 = arrow.Arrow(2013, 3, 31) + q2 = arrow.Arrow(2013, 6, 30) + q3 = arrow.Arrow(2013, 9, 30) + q4 = arrow.Arrow(2013, 12, 31) + self.assertEqual(q1.quarter, 1) + self.assertEqual(q2.quarter, 2) + self.assertEqual(q3.quarter, 3) + self.assertEqual(q4.quarter, 4) + + def test_getattr_dt_value(self): + + self.assertEqual(self.arrow.year, 2013) + + def test_tzinfo(self): + + self.arrow.tzinfo = tz.gettz("PST") + self.assertEqual(self.arrow.tzinfo, tz.gettz("PST")) + + def test_naive(self): + + self.assertEqual(self.arrow.naive, self.arrow._datetime.replace(tzinfo=None)) + + def test_timestamp(self): + + self.assertEqual( + self.arrow.timestamp, calendar.timegm(self.arrow._datetime.utctimetuple()) + ) + + def test_float_timestamp(self): + + result = self.arrow.float_timestamp - self.arrow.timestamp + + self.assertEqual(result, self.arrow.microsecond) + + +class ArrowComparisonTests(Chai): + def setUp(self): + super(ArrowComparisonTests, self).setUp() + + self.arrow = arrow.Arrow.utcnow() + + def test_eq(self): + + self.assertTrue(self.arrow == self.arrow) + self.assertTrue(self.arrow == self.arrow.datetime) + self.assertFalse(self.arrow == "abc") + + def test_ne(self): + + self.assertFalse(self.arrow != self.arrow) + self.assertFalse(self.arrow != self.arrow.datetime) + self.assertTrue(self.arrow != "abc") + + def test_gt(self): + + arrow_cmp = self.arrow.shift(minutes=1) + + self.assertFalse(self.arrow > self.arrow) + self.assertFalse(self.arrow > self.arrow.datetime) + + with self.assertRaises(TypeError): + self.arrow > "abc" + + self.assertTrue(self.arrow < arrow_cmp) + self.assertTrue(self.arrow < arrow_cmp.datetime) + + def test_ge(self): + + with self.assertRaises(TypeError): + self.arrow >= "abc" + + self.assertTrue(self.arrow >= self.arrow) + self.assertTrue(self.arrow >= self.arrow.datetime) + + def test_lt(self): + + arrow_cmp = self.arrow.shift(minutes=1) + + self.assertFalse(self.arrow < self.arrow) + self.assertFalse(self.arrow < self.arrow.datetime) + + with self.assertRaises(TypeError): + self.arrow < "abc" + + self.assertTrue(self.arrow < arrow_cmp) + self.assertTrue(self.arrow < arrow_cmp.datetime) + + def test_le(self): + + with self.assertRaises(TypeError): + self.arrow <= "abc" + + self.assertTrue(self.arrow <= self.arrow) + self.assertTrue(self.arrow <= self.arrow.datetime) + + +class ArrowMathTests(Chai): + def setUp(self): + super(ArrowMathTests, self).setUp() + + self.arrow = arrow.Arrow(2013, 1, 1) + + def test_add_timedelta(self): + + result = self.arrow.__add__(timedelta(days=1)) + + self.assertEqual(result._datetime, datetime(2013, 1, 2, tzinfo=tz.tzutc())) + + def test_add_other(self): + + with self.assertRaises(TypeError): + self.arrow + 1 + + def test_radd(self): + + result = self.arrow.__radd__(timedelta(days=1)) + + self.assertEqual(result._datetime, datetime(2013, 1, 2, tzinfo=tz.tzutc())) + + def test_sub_timedelta(self): + + result = self.arrow.__sub__(timedelta(days=1)) + + self.assertEqual(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())) + + self.assertEqual(result, timedelta(days=11)) + + def test_sub_arrow(self): + + result = self.arrow.__sub__(arrow.Arrow(2012, 12, 21, tzinfo=tz.tzutc())) + + self.assertEqual(result, timedelta(days=11)) + + def test_sub_other(self): + + with self.assertRaises(TypeError): + self.arrow - object() + + def test_rsub_datetime(self): + + result = self.arrow.__rsub__(datetime(2012, 12, 21, tzinfo=tz.tzutc())) + + self.assertEqual(result, timedelta(days=-11)) + + def test_rsub_other(self): + + with self.assertRaises(TypeError): + timedelta(days=1) - self.arrow + + +class ArrowDatetimeInterfaceTests(Chai): + def setUp(self): + super(ArrowDatetimeInterfaceTests, self).setUp() + + self.arrow = arrow.Arrow.utcnow() + + def test_date(self): + + result = self.arrow.date() + + self.assertEqual(result, self.arrow._datetime.date()) + + def test_time(self): + + result = self.arrow.time() + + self.assertEqual(result, self.arrow._datetime.time()) + + def test_timetz(self): + + result = self.arrow.timetz() + + self.assertEqual(result, self.arrow._datetime.timetz()) + + def test_astimezone(self): + + other_tz = tz.gettz("US/Pacific") + + result = self.arrow.astimezone(other_tz) + + self.assertEqual(result, self.arrow._datetime.astimezone(other_tz)) + + def test_utcoffset(self): + + result = self.arrow.utcoffset() + + self.assertEqual(result, self.arrow._datetime.utcoffset()) + + def test_dst(self): + + result = self.arrow.dst() + + self.assertEqual(result, self.arrow._datetime.dst()) + + def test_timetuple(self): + + result = self.arrow.timetuple() + + self.assertEqual(result, self.arrow._datetime.timetuple()) + + def test_utctimetuple(self): + + result = self.arrow.utctimetuple() + + self.assertEqual(result, self.arrow._datetime.utctimetuple()) + + def test_toordinal(self): + + result = self.arrow.toordinal() + + self.assertEqual(result, self.arrow._datetime.toordinal()) + + def test_weekday(self): + + result = self.arrow.weekday() + + self.assertEqual(result, self.arrow._datetime.weekday()) + + def test_isoweekday(self): + + result = self.arrow.isoweekday() + + self.assertEqual(result, self.arrow._datetime.isoweekday()) + + def test_isocalendar(self): + + result = self.arrow.isocalendar() + + self.assertEqual(result, self.arrow._datetime.isocalendar()) + + def test_isoformat(self): + + result = self.arrow.isoformat() + + self.assertEqual(result, self.arrow._datetime.isoformat()) + + def test_simplejson(self): + + result = json.dumps({"v": self.arrow.for_json()}, for_json=True) + + self.assertEqual(json.loads(result)["v"], self.arrow._datetime.isoformat()) + + def test_ctime(self): + + result = self.arrow.ctime() + + self.assertEqual(result, self.arrow._datetime.ctime()) + + def test_strftime(self): + + result = self.arrow.strftime("%Y") + + self.assertEqual(result, self.arrow._datetime.strftime("%Y")) + + +class ArrowFalsePositiveDstTests(Chai): + """These tests relate to issues #376 and #551. + The key points in both issues are that arrow will assign a UTC timezone if none is provided and + .to() will change other attributes to be correct whereas .replace() only changes the specified attribute. + + Issue 376 + >>> arrow.get('2016-11-06').to('America/New_York').ceil('day') + < Arrow [2016-11-05T23:59:59.999999-04:00] > + + Issue 551 + >>> just_before = arrow.get('2018-11-04T01:59:59.999999') + >>> just_before + 2018-11-04T01:59:59.999999+00:00 + >>> just_after = just_before.shift(microseconds=1) + >>> just_after + 2018-11-04T02:00:00+00:00 + >>> just_before_eastern = just_before.replace(tzinfo='US/Eastern') + >>> just_before_eastern + 2018-11-04T01:59:59.999999-04:00 + >>> just_after_eastern = just_after.replace(tzinfo='US/Eastern') + >>> just_after_eastern + 2018-11-04T02:00:00-05:00 + """ + + def setUp(self): + + super(ArrowFalsePositiveDstTests, self).setUp() + self.before_1 = arrow.Arrow( + 2016, 11, 6, 3, 59, tzinfo=tz.gettz("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.after_2 = arrow.Arrow( + 2016, 11, 6, 23, 59, tzinfo=tz.gettz("America/New_York") + ) + self.before_3 = arrow.Arrow( + 2018, 11, 4, 3, 59, tzinfo=tz.gettz("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.after_4 = arrow.Arrow( + 2018, 11, 4, 23, 59, tzinfo=tz.gettz("America/New_York") + ) + + def test_dst(self): + + self.assertEqual(self.before_1.day, self.before_2.day) + self.assertEqual(self.after_1.day, self.after_2.day) + self.assertEqual(self.before_3.day, self.before_4.day) + self.assertEqual(self.after_3.day, self.after_4.day) + + +class ArrowConversionTests(Chai): + def test_to(self): + + dt_from = datetime.now() + arrow_from = arrow.Arrow.fromdatetime(dt_from, tz.gettz("US/Pacific")) + + self.expected = dt_from.replace(tzinfo=tz.gettz("US/Pacific")).astimezone( + tz.tzutc() + ) + + self.assertEqual(arrow_from.to("UTC").datetime, self.expected) + self.assertEqual(arrow_from.to(tz.tzutc()).datetime, self.expected) + + +class ArrowPicklingTests(Chai): + def test_pickle_and_unpickle(self): + + dt = arrow.Arrow.utcnow() + + pickled = pickle.dumps(dt) + + unpickled = pickle.loads(pickled) + + self.assertEqual(unpickled, dt) + + +class ArrowReplaceTests(Chai): + def test_not_attr(self): + + with self.assertRaises(AttributeError): + arrow.Arrow.utcnow().replace(abc=1) + + def test_replace(self): + + arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) + + self.assertEqual(arw.replace(year=2012), arrow.Arrow(2012, 5, 5, 12, 30, 45)) + self.assertEqual(arw.replace(month=1), arrow.Arrow(2013, 1, 5, 12, 30, 45)) + self.assertEqual(arw.replace(day=1), arrow.Arrow(2013, 5, 1, 12, 30, 45)) + self.assertEqual(arw.replace(hour=1), arrow.Arrow(2013, 5, 5, 1, 30, 45)) + self.assertEqual(arw.replace(minute=1), arrow.Arrow(2013, 5, 5, 12, 1, 45)) + self.assertEqual(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")) + + self.assertEqual(result, arw.datetime.replace(tzinfo=tz.gettz("US/Pacific"))) + + def test_replace_week(self): + + with self.assertRaises(AttributeError): + arrow.Arrow.utcnow().replace(week=1) + + def test_replace_quarter(self): + + with self.assertRaises(AttributeError): + arrow.Arrow.utcnow().replace(quarter=1) + + def test_replace_other_kwargs(self): + + with self.assertRaises(AttributeError): + arrow.utcnow().replace(abc="def") + + +class ArrowShiftTests(Chai): + def test_not_attr(self): + + now = arrow.Arrow.utcnow() + + with self.assertRaises(AttributeError): + now.shift(abc=1) + + with self.assertRaises(AttributeError): + now.shift(week=1) + + def test_shift(self): + + arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) + + self.assertEqual(arw.shift(years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) + self.assertEqual(arw.shift(quarters=1), arrow.Arrow(2013, 8, 5, 12, 30, 45)) + self.assertEqual( + arw.shift(quarters=1, months=1), arrow.Arrow(2013, 9, 5, 12, 30, 45) + ) + self.assertEqual(arw.shift(months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) + self.assertEqual(arw.shift(weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) + self.assertEqual(arw.shift(days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + self.assertEqual(arw.shift(hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) + self.assertEqual(arw.shift(minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) + self.assertEqual(arw.shift(seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) + self.assertEqual( + arw.shift(microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1) + ) + + # Remember: Python's weekday 0 is Monday + self.assertEqual(arw.shift(weekday=0), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=1), arrow.Arrow(2013, 5, 7, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=2), arrow.Arrow(2013, 5, 8, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=3), arrow.Arrow(2013, 5, 9, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=4), arrow.Arrow(2013, 5, 10, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=5), arrow.Arrow(2013, 5, 11, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=6), arw) + + with self.assertRaises(IndexError): + arw.shift(weekday=7) + + # Use dateutil.relativedelta's convenient day instances + self.assertEqual(arw.shift(weekday=MO), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=MO(0)), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=MO(1)), arrow.Arrow(2013, 5, 6, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=MO(2)), arrow.Arrow(2013, 5, 13, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TU), arrow.Arrow(2013, 5, 7, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TU(0)), arrow.Arrow(2013, 5, 7, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TU(1)), arrow.Arrow(2013, 5, 7, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TU(2)), arrow.Arrow(2013, 5, 14, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=WE), arrow.Arrow(2013, 5, 8, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=WE(0)), arrow.Arrow(2013, 5, 8, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=WE(1)), arrow.Arrow(2013, 5, 8, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=WE(2)), arrow.Arrow(2013, 5, 15, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TH), arrow.Arrow(2013, 5, 9, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TH(0)), arrow.Arrow(2013, 5, 9, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TH(1)), arrow.Arrow(2013, 5, 9, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TH(2)), arrow.Arrow(2013, 5, 16, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=FR), arrow.Arrow(2013, 5, 10, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=FR(0)), arrow.Arrow(2013, 5, 10, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=FR(1)), arrow.Arrow(2013, 5, 10, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=FR(2)), arrow.Arrow(2013, 5, 17, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=SA), arrow.Arrow(2013, 5, 11, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=SA(0)), arrow.Arrow(2013, 5, 11, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=SA(1)), arrow.Arrow(2013, 5, 11, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=SA(2)), arrow.Arrow(2013, 5, 18, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=SU), arw) + self.assertEqual(arw.shift(weekday=SU(0)), arw) + self.assertEqual(arw.shift(weekday=SU(1)), arw) + self.assertEqual(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) + + self.assertEqual(arw.shift(years=-1), arrow.Arrow(2012, 5, 5, 12, 30, 45)) + self.assertEqual(arw.shift(quarters=-1), arrow.Arrow(2013, 2, 5, 12, 30, 45)) + self.assertEqual( + arw.shift(quarters=-1, months=-1), arrow.Arrow(2013, 1, 5, 12, 30, 45) + ) + self.assertEqual(arw.shift(months=-1), arrow.Arrow(2013, 4, 5, 12, 30, 45)) + self.assertEqual(arw.shift(weeks=-1), arrow.Arrow(2013, 4, 28, 12, 30, 45)) + self.assertEqual(arw.shift(days=-1), arrow.Arrow(2013, 5, 4, 12, 30, 45)) + self.assertEqual(arw.shift(hours=-1), arrow.Arrow(2013, 5, 5, 11, 30, 45)) + self.assertEqual(arw.shift(minutes=-1), arrow.Arrow(2013, 5, 5, 12, 29, 45)) + self.assertEqual(arw.shift(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) + self.assertEqual( + arw.shift(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999) + ) + + # Not sure how practical these negative weekdays are + self.assertEqual(arw.shift(weekday=-1), arw.shift(weekday=SU)) + self.assertEqual(arw.shift(weekday=-2), arw.shift(weekday=SA)) + self.assertEqual(arw.shift(weekday=-3), arw.shift(weekday=FR)) + self.assertEqual(arw.shift(weekday=-4), arw.shift(weekday=TH)) + self.assertEqual(arw.shift(weekday=-5), arw.shift(weekday=WE)) + self.assertEqual(arw.shift(weekday=-6), arw.shift(weekday=TU)) + self.assertEqual(arw.shift(weekday=-7), arw.shift(weekday=MO)) + + with self.assertRaises(IndexError): + arw.shift(weekday=-8) + + self.assertEqual( + arw.shift(weekday=MO(-1)), arrow.Arrow(2013, 4, 29, 12, 30, 45) + ) + self.assertEqual( + arw.shift(weekday=TU(-1)), arrow.Arrow(2013, 4, 30, 12, 30, 45) + ) + self.assertEqual(arw.shift(weekday=WE(-1)), arrow.Arrow(2013, 5, 1, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=TH(-1)), arrow.Arrow(2013, 5, 2, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=FR(-1)), arrow.Arrow(2013, 5, 3, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=SA(-1)), arrow.Arrow(2013, 5, 4, 12, 30, 45)) + self.assertEqual(arw.shift(weekday=SU(-1)), arw) + self.assertEqual( + 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. + # Recall that the keyword argument dict, like all dicts, is unordered, so only certain + # combinations of arguments would exhibit this. + self.assertEqual( + arw.shift(quarters=0, years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45) + ) + self.assertEqual( + arw.shift(quarters=0, months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45) + ) + self.assertEqual( + arw.shift(quarters=0, weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45) + ) + self.assertEqual( + arw.shift(quarters=0, days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45) + ) + self.assertEqual( + arw.shift(quarters=0, hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45) + ) + self.assertEqual( + arw.shift(quarters=0, minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45) + ) + self.assertEqual( + arw.shift(quarters=0, seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46) + ) + self.assertEqual( + arw.shift(quarters=0, microseconds=1), + arrow.Arrow(2013, 5, 5, 12, 30, 45, 1), + ) + + +class ArrowRangeTests(Chai): + def test_year(self): + + result = list( + arrow.Arrow.range( + "year", datetime(2013, 1, 2, 3, 4, 5), datetime(2016, 4, 5, 6, 7, 8) + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2014, 1, 2, 3, 4, 5), + arrow.Arrow(2015, 1, 2, 3, 4, 5), + arrow.Arrow(2016, 1, 2, 3, 4, 5), + ], + ) + + def test_quarter(self): + + result = list( + arrow.Arrow.range( + "quarter", datetime(2013, 2, 3, 4, 5, 6), datetime(2013, 5, 6, 7, 8, 9) + ) + ) + + self.assertEqual( + result, [arrow.Arrow(2013, 2, 3, 4, 5, 6), arrow.Arrow(2013, 5, 3, 4, 5, 6)] + ) + + def test_month(self): + + result = list( + arrow.Arrow.range( + "month", datetime(2013, 2, 3, 4, 5, 6), datetime(2013, 5, 6, 7, 8, 9) + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 2, 3, 4, 5, 6), + arrow.Arrow(2013, 3, 3, 4, 5, 6), + arrow.Arrow(2013, 4, 3, 4, 5, 6), + arrow.Arrow(2013, 5, 3, 4, 5, 6), + ], + ) + + def test_week(self): + + result = list( + arrow.Arrow.range( + "week", datetime(2013, 9, 1, 2, 3, 4), datetime(2013, 10, 1, 2, 3, 4) + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 9, 1, 2, 3, 4), + arrow.Arrow(2013, 9, 8, 2, 3, 4), + arrow.Arrow(2013, 9, 15, 2, 3, 4), + arrow.Arrow(2013, 9, 22, 2, 3, 4), + arrow.Arrow(2013, 9, 29, 2, 3, 4), + ], + ) + + def test_day(self): + + result = list( + arrow.Arrow.range( + "day", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 5, 6, 7, 8) + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 3, 3, 4, 5), + arrow.Arrow(2013, 1, 4, 3, 4, 5), + arrow.Arrow(2013, 1, 5, 3, 4, 5), + ], + ) + + def test_hour(self): + + result = list( + arrow.Arrow.range( + "hour", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 6, 7, 8) + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 2, 4, 4, 5), + arrow.Arrow(2013, 1, 2, 5, 4, 5), + arrow.Arrow(2013, 1, 2, 6, 4, 5), + ], + ) + + result = list( + arrow.Arrow.range( + "hour", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 3, 4, 5) + ) + ) + + self.assertEqual(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) + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 2, 3, 5, 5), + arrow.Arrow(2013, 1, 2, 3, 6, 5), + arrow.Arrow(2013, 1, 2, 3, 7, 5), + ], + ) + + def test_second(self): + + result = list( + arrow.Arrow.range( + "second", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 3, 4, 8) + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 2, 3, 4, 6), + arrow.Arrow(2013, 1, 2, 3, 4, 7), + arrow.Arrow(2013, 1, 2, 3, 4, 8), + ], + ) + + def test_arrow(self): + + result = list( + arrow.Arrow.range( + "day", + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 5, 6, 7, 8), + ) + ) + + self.assertEqual( + result, + [ + arrow.Arrow(2013, 1, 2, 3, 4, 5), + arrow.Arrow(2013, 1, 3, 3, 4, 5), + arrow.Arrow(2013, 1, 4, 3, 4, 5), + arrow.Arrow(2013, 1, 5, 3, 4, 5), + ], + ) + + def test_naive_tz(self): + + result = arrow.Arrow.range( + "year", datetime(2013, 1, 2, 3), datetime(2016, 4, 5, 6), "US/Pacific" + ) + + [self.assertEqual(r.tzinfo, tz.gettz("US/Pacific")) for r in result] + + 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")), + ) + + [self.assertEqual(r.tzinfo, tz.gettz("US/Pacific")) for r in result] + + 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")), + ) + + [self.assertEqual(r.tzinfo, tz.gettz("US/Eastern")) for r in result] + + 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"), + ) + + [self.assertEqual(r.tzinfo, tz.gettz("US/Central")) for r in result] + + def test_unsupported(self): + + with self.assertRaises(AttributeError): + next(arrow.Arrow.range("abc", datetime.utcnow(), datetime.utcnow())) + + +class ArrowSpanRangeTests(Chai): + def test_year(self): + + result = list( + arrow.Arrow.span_range("year", datetime(2013, 2, 1), datetime(2016, 3, 31)) + ) + + self.assertEqual( + result, + [ + ( + arrow.Arrow(2013, 1, 1), + arrow.Arrow(2013, 12, 31, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2014, 1, 1), + arrow.Arrow(2014, 12, 31, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2015, 1, 1), + arrow.Arrow(2015, 12, 31, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2016, 1, 1), + arrow.Arrow(2016, 12, 31, 23, 59, 59, 999999), + ), + ], + ) + + def test_quarter(self): + + result = list( + arrow.Arrow.span_range( + "quarter", datetime(2013, 2, 2), datetime(2013, 5, 15) + ) + ) + + self.assertEqual( + result, + [ + (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 6, 30, 23, 59, 59, 999999)), + ], + ) + + def test_month(self): + + result = list( + arrow.Arrow.span_range("month", datetime(2013, 1, 2), datetime(2013, 4, 15)) + ) + + self.assertEqual( + result, + [ + (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 1, 31, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 2, 1), arrow.Arrow(2013, 2, 28, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 3, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 4, 30, 23, 59, 59, 999999)), + ], + ) + + def test_week(self): + + result = list( + arrow.Arrow.span_range("week", datetime(2013, 2, 2), datetime(2013, 2, 28)) + ) + + self.assertEqual( + result, + [ + (arrow.Arrow(2013, 1, 28), arrow.Arrow(2013, 2, 3, 23, 59, 59, 999999)), + (arrow.Arrow(2013, 2, 4), arrow.Arrow(2013, 2, 10, 23, 59, 59, 999999)), + ( + arrow.Arrow(2013, 2, 11), + arrow.Arrow(2013, 2, 17, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 2, 18), + arrow.Arrow(2013, 2, 24, 23, 59, 59, 999999), + ), + (arrow.Arrow(2013, 2, 25), arrow.Arrow(2013, 3, 3, 23, 59, 59, 999999)), + ], + ) + + def test_day(self): + + result = list( + arrow.Arrow.span_range( + "day", datetime(2013, 1, 1, 12), datetime(2013, 1, 4, 12) + ) + ) + + self.assertEqual( + result, + [ + ( + arrow.Arrow(2013, 1, 1, 0), + arrow.Arrow(2013, 1, 1, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 2, 0), + arrow.Arrow(2013, 1, 2, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 3, 0), + arrow.Arrow(2013, 1, 3, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 4, 0), + arrow.Arrow(2013, 1, 4, 23, 59, 59, 999999), + ), + ], + ) + + def test_days(self): + + result = list( + arrow.Arrow.span_range( + "days", datetime(2013, 1, 1, 12), datetime(2013, 1, 4, 12) + ) + ) + + self.assertEqual( + result, + [ + ( + arrow.Arrow(2013, 1, 1, 0), + arrow.Arrow(2013, 1, 1, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 2, 0), + arrow.Arrow(2013, 1, 2, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 3, 0), + arrow.Arrow(2013, 1, 3, 23, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 4, 0), + arrow.Arrow(2013, 1, 4, 23, 59, 59, 999999), + ), + ], + ) + + def test_hour(self): + + result = list( + arrow.Arrow.span_range( + "hour", datetime(2013, 1, 1, 0, 30), datetime(2013, 1, 1, 3, 30) + ) + ) + + self.assertEqual( + result, + [ + ( + arrow.Arrow(2013, 1, 1, 0), + arrow.Arrow(2013, 1, 1, 0, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 1), + arrow.Arrow(2013, 1, 1, 1, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 2), + arrow.Arrow(2013, 1, 1, 2, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 3), + arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999), + ), + ], + ) + + result = list( + arrow.Arrow.span_range( + "hour", datetime(2013, 1, 1, 3, 30), datetime(2013, 1, 1, 3, 30) + ) + ) + + self.assertEqual( + result, + [(arrow.Arrow(2013, 1, 1, 3), arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999))], + ) + + 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) + ) + ) + + self.assertEqual( + result, + [ + ( + arrow.Arrow(2013, 1, 1, 0, 0), + arrow.Arrow(2013, 1, 1, 0, 0, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 0, 1), + arrow.Arrow(2013, 1, 1, 0, 1, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 0, 2), + arrow.Arrow(2013, 1, 1, 0, 2, 59, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 0, 3), + arrow.Arrow(2013, 1, 1, 0, 3, 59, 999999), + ), + ], + ) + + def test_second(self): + + result = list( + arrow.Arrow.span_range( + "second", datetime(2013, 1, 1), datetime(2013, 1, 1, 0, 0, 3) + ) + ) + + self.assertEqual( + result, + [ + ( + arrow.Arrow(2013, 1, 1, 0, 0, 0), + arrow.Arrow(2013, 1, 1, 0, 0, 0, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 0, 0, 1), + arrow.Arrow(2013, 1, 1, 0, 0, 1, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 0, 0, 2), + arrow.Arrow(2013, 1, 1, 0, 0, 2, 999999), + ), + ( + arrow.Arrow(2013, 1, 1, 0, 0, 3), + arrow.Arrow(2013, 1, 1, 0, 0, 3, 999999), + ), + ], + ) + + def test_naive_tz(self): + + tzinfo = tz.gettz("US/Pacific") + + result = arrow.Arrow.span_range( + "hour", datetime(2013, 1, 1, 0), datetime(2013, 1, 1, 3, 59), "US/Pacific" + ) + + for f, c in result: + self.assertEqual(f.tzinfo, tzinfo) + self.assertEqual(c.tzinfo, tzinfo) + + def test_aware_same_tz(self): + + tzinfo = tz.gettz("US/Pacific") + + result = arrow.Arrow.span_range( + "hour", + datetime(2013, 1, 1, 0, tzinfo=tzinfo), + datetime(2013, 1, 1, 2, 59, tzinfo=tzinfo), + ) + + for f, c in result: + self.assertEqual(f.tzinfo, tzinfo) + self.assertEqual(c.tzinfo, tzinfo) + + def test_aware_different_tz(self): + + tzinfo1 = tz.gettz("US/Pacific") + tzinfo2 = tz.gettz("US/Eastern") + + result = arrow.Arrow.span_range( + "hour", + datetime(2013, 1, 1, 0, tzinfo=tzinfo1), + datetime(2013, 1, 1, 2, 59, tzinfo=tzinfo2), + ) + + for f, c in result: + self.assertEqual(f.tzinfo, tzinfo1) + self.assertEqual(c.tzinfo, tzinfo1) + + 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")), + tz="US/Central", + ) + + for f, c in result: + self.assertEqual(f.tzinfo, tz.gettz("US/Central")) + self.assertEqual(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="[]" + ) + ) + + self.assertEqual( + result, + [ + (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 4, 1)), + (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 7, 1)), + ], + ) + + +class ArrowIntervalTests(Chai): + def test_incorrect_input(self): + correct = True + try: + list( + arrow.Arrow.interval( + "month", datetime(2013, 1, 2), datetime(2013, 4, 15), 0 + ) + ) + except: # noqa: E722 + correct = False + + self.assertEqual(correct, False) + + def test_correct(self): + result = list( + arrow.Arrow.interval( + "hour", datetime(2013, 5, 5, 12, 30), datetime(2013, 5, 5, 17, 15), 2 + ) + ) + + self.assertEqual( + result, + [ + ( + arrow.Arrow(2013, 5, 5, 12), + arrow.Arrow(2013, 5, 5, 13, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 14), + arrow.Arrow(2013, 5, 5, 15, 59, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 16), + arrow.Arrow(2013, 5, 5, 17, 59, 59, 999999), + ), + ], + ) + + def test_bounds_param_is_passed(self): + result = list( + arrow.Arrow.interval( + "hour", + datetime(2013, 5, 5, 12, 30), + datetime(2013, 5, 5, 17, 15), + 2, + bounds="[]", + ) + ) + + self.assertEqual( + result, + [ + (arrow.Arrow(2013, 5, 5, 12), arrow.Arrow(2013, 5, 5, 14)), + (arrow.Arrow(2013, 5, 5, 14), arrow.Arrow(2013, 5, 5, 16)), + (arrow.Arrow(2013, 5, 5, 16), arrow.Arrow(2013, 5, 5, 18)), + ], + ) + + +class ArrowSpanTests(Chai): + def setUp(self): + super(ArrowSpanTests, self).setUp() + + self.datetime = datetime(2013, 2, 15, 3, 41, 22, 8923) + self.arrow = arrow.Arrow.fromdatetime(self.datetime) + + def test_span_attribute(self): + + with self.assertRaises(AttributeError): + self.arrow.span("span") + + def test_span_year(self): + + floor, ceil = self.arrow.span("year") + + self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + ) + + def test_span_quarter(self): + + floor, ceil = self.arrow.span("quarter") + + self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + self.assertEqual( + 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) + + self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + self.assertEqual( + 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) + + self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2014, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + ) + + def test_span_month(self): + + floor, ceil = self.arrow.span("month") + + self.assertEqual(floor, datetime(2013, 2, 1, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 2, 28, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + ) + + def test_span_week(self): + + floor, ceil = self.arrow.span("week") + + self.assertEqual(floor, datetime(2013, 2, 11, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 2, 17, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + ) + + def test_span_day(self): + + floor, ceil = self.arrow.span("day") + + self.assertEqual(floor, datetime(2013, 2, 15, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 2, 15, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + ) + + def test_span_hour(self): + + floor, ceil = self.arrow.span("hour") + + self.assertEqual(floor, datetime(2013, 2, 15, 3, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 2, 15, 3, 59, 59, 999999, tzinfo=tz.tzutc()) + ) + + def test_span_minute(self): + + floor, ceil = self.arrow.span("minute") + + self.assertEqual(floor, datetime(2013, 2, 15, 3, 41, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 2, 15, 3, 41, 59, 999999, tzinfo=tz.tzutc()) + ) + + def test_span_second(self): + + floor, ceil = self.arrow.span("second") + + self.assertEqual(floor, datetime(2013, 2, 15, 3, 41, 22, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 2, 15, 3, 41, 22, 999999, tzinfo=tz.tzutc()) + ) + + def test_span_microsecond(self): + + floor, ceil = self.arrow.span("microsecond") + + self.assertEqual( + floor, datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) + ) + self.assertEqual( + ceil, datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) + ) + + def test_floor(self): + + floor, ceil = self.arrow.span("month") + + self.assertEqual(floor, self.arrow.floor("month")) + self.assertEqual(ceil, self.arrow.ceil("month")) + + def test_span_inclusive_inclusive(self): + + floor, ceil = self.arrow.span("hour", bounds="[]") + + self.assertEqual(floor, datetime(2013, 2, 15, 3, tzinfo=tz.tzutc())) + self.assertEqual(ceil, datetime(2013, 2, 15, 4, tzinfo=tz.tzutc())) + + def test_span_exclusive_inclusive(self): + + floor, ceil = self.arrow.span("hour", bounds="(]") + + self.assertEqual(floor, datetime(2013, 2, 15, 3, 0, 0, 1, tzinfo=tz.tzutc())) + self.assertEqual(ceil, datetime(2013, 2, 15, 4, tzinfo=tz.tzutc())) + + def test_span_exclusive_exclusive(self): + + floor, ceil = self.arrow.span("hour", bounds="()") + + self.assertEqual(floor, datetime(2013, 2, 15, 3, 0, 0, 1, tzinfo=tz.tzutc())) + self.assertEqual( + ceil, datetime(2013, 2, 15, 3, 59, 59, 999999, tzinfo=tz.tzutc()) + ) + + def test_bounds_are_validated(self): + + with self.assertRaises(AttributeError): + floor, ceil = self.arrow.span("hour", bounds="][") + + +class ArrowHumanizeTests(Chai): + def setUp(self): + super(ArrowHumanizeTests, self).setUp() + + self.datetime = datetime(2013, 1, 1) + self.now = arrow.Arrow.utcnow() + + def test_granularity(self): + + self.assertEqual(self.now.humanize(granularity="second"), "just now") + + later1 = self.now.shift(seconds=1) + self.assertEqual(self.now.humanize(later1, granularity="second"), "just now") + self.assertEqual(later1.humanize(self.now, granularity="second"), "just now") + self.assertEqual( + self.now.humanize(later1, granularity="minute"), "0 minutes ago" + ) + self.assertEqual( + later1.humanize(self.now, granularity="minute"), "in 0 minutes" + ) + + later100 = self.now.shift(seconds=100) + self.assertEqual( + self.now.humanize(later100, granularity="second"), "100 seconds ago" + ) + self.assertEqual( + later100.humanize(self.now, granularity="second"), "in 100 seconds" + ) + self.assertEqual( + self.now.humanize(later100, granularity="minute"), "a minute ago" + ) + self.assertEqual( + later100.humanize(self.now, granularity="minute"), "in a minute" + ) + self.assertEqual(self.now.humanize(later100, granularity="hour"), "0 hours ago") + self.assertEqual(later100.humanize(self.now, granularity="hour"), "in 0 hours") + + later4000 = self.now.shift(seconds=4000) + self.assertEqual( + self.now.humanize(later4000, granularity="minute"), "66 minutes ago" + ) + self.assertEqual( + later4000.humanize(self.now, granularity="minute"), "in 66 minutes" + ) + self.assertEqual( + self.now.humanize(later4000, granularity="hour"), "an hour ago" + ) + self.assertEqual(later4000.humanize(self.now, granularity="hour"), "in an hour") + self.assertEqual(self.now.humanize(later4000, granularity="day"), "0 days ago") + self.assertEqual(later4000.humanize(self.now, granularity="day"), "in 0 days") + + later105 = self.now.shift(seconds=10 ** 5) + self.assertEqual( + self.now.humanize(later105, granularity="hour"), "27 hours ago" + ) + self.assertEqual(later105.humanize(self.now, granularity="hour"), "in 27 hours") + self.assertEqual(self.now.humanize(later105, granularity="day"), "a day ago") + self.assertEqual(later105.humanize(self.now, granularity="day"), "in a day") + self.assertEqual(self.now.humanize(later105, granularity="week"), "0 weeks ago") + self.assertEqual(later105.humanize(self.now, granularity="week"), "in 0 weeks") + self.assertEqual( + self.now.humanize(later105, granularity="month"), "0 months ago" + ) + self.assertEqual( + later105.humanize(self.now, granularity="month"), "in 0 months" + ) + self.assertEqual( + self.now.humanize(later105, granularity=["month"]), "0 months ago" + ) + self.assertEqual( + later105.humanize(self.now, granularity=["month"]), "in 0 months" + ) + + later106 = self.now.shift(seconds=3 * 10 ** 6) + self.assertEqual(self.now.humanize(later106, granularity="day"), "34 days ago") + self.assertEqual(later106.humanize(self.now, granularity="day"), "in 34 days") + self.assertEqual(self.now.humanize(later106, granularity="week"), "4 weeks ago") + self.assertEqual(later106.humanize(self.now, granularity="week"), "in 4 weeks") + self.assertEqual( + self.now.humanize(later106, granularity="month"), "a month ago" + ) + self.assertEqual(later106.humanize(self.now, granularity="month"), "in a month") + self.assertEqual(self.now.humanize(later106, granularity="year"), "0 years ago") + self.assertEqual(later106.humanize(self.now, granularity="year"), "in 0 years") + + later506 = self.now.shift(seconds=50 * 10 ** 6) + self.assertEqual( + self.now.humanize(later506, granularity="week"), "82 weeks ago" + ) + self.assertEqual(later506.humanize(self.now, granularity="week"), "in 82 weeks") + self.assertEqual( + self.now.humanize(later506, granularity="month"), "18 months ago" + ) + self.assertEqual( + later506.humanize(self.now, granularity="month"), "in 18 months" + ) + self.assertEqual(self.now.humanize(later506, granularity="year"), "a year ago") + self.assertEqual(later506.humanize(self.now, granularity="year"), "in a year") + + later108 = self.now.shift(seconds=10 ** 8) + self.assertEqual(self.now.humanize(later108, granularity="year"), "3 years ago") + self.assertEqual(later108.humanize(self.now, granularity="year"), "in 3 years") + + later108onlydistance = self.now.shift(seconds=10 ** 8) + self.assertEqual( + self.now.humanize( + later108onlydistance, only_distance=True, granularity="year" + ), + "3 years", + ) + self.assertEqual( + later108onlydistance.humanize( + self.now, only_distance=True, granularity="year" + ), + "3 years", + ) + + with self.assertRaises(AttributeError): + self.now.humanize(later108, granularity="years") + + def test_multiple_granularity(self): + self.assertEqual(self.now.humanize(granularity="second"), "just now") + self.assertEqual(self.now.humanize(granularity=["second"]), "just now") + self.assertEqual( + self.now.humanize(granularity=["year", "month", "day", "hour", "second"]), + "in 0 years 0 months 0 days 0 hours and 0 seconds", + ) + + later4000 = self.now.shift(seconds=4000) + self.assertEqual( + later4000.humanize(self.now, granularity=["hour", "minute"]), + "in an hour and 6 minutes", + ) + self.assertEqual( + self.now.humanize(later4000, granularity=["hour", "minute"]), + "an hour and 6 minutes ago", + ) + self.assertEqual( + later4000.humanize( + self.now, granularity=["hour", "minute"], only_distance=True + ), + "an hour and 6 minutes", + ) + self.assertEqual( + later4000.humanize(self.now, granularity=["day", "hour", "minute"]), + "in 0 days an hour and 6 minutes", + ) + self.assertEqual( + self.now.humanize(later4000, granularity=["day", "hour", "minute"]), + "0 days an hour and 6 minutes ago", + ) + + later105 = self.now.shift(seconds=10 ** 5) + self.assertEqual( + self.now.humanize(later105, granularity=["hour", "day", "minute"]), + "a day 3 hours and 46 minutes ago", + ) + with self.assertRaises(AttributeError): + self.now.humanize(later105, granularity=["error", "second"]) + + later108onlydistance = self.now.shift(seconds=10 ** 8) + self.assertEqual( + self.now.humanize( + later108onlydistance, only_distance=True, granularity=["year"] + ), + "3 years", + ) + self.assertEqual( + self.now.humanize( + later108onlydistance, only_distance=True, granularity=["month", "week"] + ), + "37 months and 4 weeks", + ) + self.assertEqual( + self.now.humanize( + later108onlydistance, only_distance=True, granularity=["year", "second"] + ), + "3 years and 5327200 seconds", + ) + + one_min_one_sec_ago = self.now.shift(minutes=-1, seconds=-1) + self.assertEqual( + one_min_one_sec_ago.humanize(self.now, granularity=["minute", "second"]), + "a minute and a second ago", + ) + + one_min_two_secs_ago = self.now.shift(minutes=-1, seconds=-2) + self.assertEqual( + one_min_two_secs_ago.humanize(self.now, granularity=["minute", "second"]), + "a minute and 2 seconds ago", + ) + + def test_seconds(self): + + later = self.now.shift(seconds=10) + + # regression test for issue #727 + self.assertEqual(self.now.humanize(later), "10 seconds ago") + self.assertEqual(later.humanize(self.now), "in 10 seconds") + + self.assertEqual(self.now.humanize(later, only_distance=True), "10 seconds") + self.assertEqual(later.humanize(self.now, only_distance=True), "10 seconds") + + def test_minute(self): + + later = self.now.shift(minutes=1) + + self.assertEqual(self.now.humanize(later), "a minute ago") + self.assertEqual(later.humanize(self.now), "in a minute") + + self.assertEqual(self.now.humanize(later, only_distance=True), "a minute") + self.assertEqual(later.humanize(self.now, only_distance=True), "a minute") + + def test_minutes(self): + + later = self.now.shift(minutes=2) + + self.assertEqual(self.now.humanize(later), "2 minutes ago") + self.assertEqual(later.humanize(self.now), "in 2 minutes") + + self.assertEqual(self.now.humanize(later, only_distance=True), "2 minutes") + self.assertEqual(later.humanize(self.now, only_distance=True), "2 minutes") + + def test_hour(self): + + later = self.now.shift(hours=1) + + self.assertEqual(self.now.humanize(later), "an hour ago") + self.assertEqual(later.humanize(self.now), "in an hour") + + self.assertEqual(self.now.humanize(later, only_distance=True), "an hour") + self.assertEqual(later.humanize(self.now, only_distance=True), "an hour") + + def test_hours(self): + + later = self.now.shift(hours=2) + + self.assertEqual(self.now.humanize(later), "2 hours ago") + self.assertEqual(later.humanize(self.now), "in 2 hours") + + self.assertEqual(self.now.humanize(later, only_distance=True), "2 hours") + self.assertEqual(later.humanize(self.now, only_distance=True), "2 hours") + + def test_day(self): + + later = self.now.shift(days=1) + + self.assertEqual(self.now.humanize(later), "a day ago") + self.assertEqual(later.humanize(self.now), "in a day") + + # regression test for issue #697 + less_than_48_hours = self.now.shift( + days=1, hours=23, seconds=59, microseconds=999999 + ) + self.assertEqual(self.now.humanize(less_than_48_hours), "a day ago") + self.assertEqual(less_than_48_hours.humanize(self.now), "in a day") + + less_than_48_hours_date = less_than_48_hours._datetime.date() + with self.assertRaises(TypeError): + # humanize other argument does not take raw datetime.date objects + self.now.humanize(less_than_48_hours_date) + + # convert from date to arrow object + less_than_48_hours_date = arrow.Arrow.fromdate(less_than_48_hours_date) + self.assertEqual(self.now.humanize(less_than_48_hours_date), "a day ago") + self.assertEqual(less_than_48_hours_date.humanize(self.now), "in a day") + + self.assertEqual(self.now.humanize(later, only_distance=True), "a day") + self.assertEqual(later.humanize(self.now, only_distance=True), "a day") + + def test_days(self): + + later = self.now.shift(days=2) + + self.assertEqual(self.now.humanize(later), "2 days ago") + self.assertEqual(later.humanize(self.now), "in 2 days") + + self.assertEqual(self.now.humanize(later, only_distance=True), "2 days") + self.assertEqual(later.humanize(self.now, only_distance=True), "2 days") + + # Regression tests for humanize bug referenced in issue 541 + later = self.now.shift(days=3) + self.assertEqual(later.humanize(), "in 3 days") + + later = self.now.shift(days=3, seconds=1) + self.assertEqual(later.humanize(), "in 3 days") + + later = self.now.shift(days=4) + self.assertEqual(later.humanize(), "in 4 days") + + def test_week(self): + + later = self.now.shift(weeks=1) + + self.assertEqual(self.now.humanize(later), "a week ago") + self.assertEqual(later.humanize(self.now), "in a week") + + self.assertEqual(self.now.humanize(later, only_distance=True), "a week") + self.assertEqual(later.humanize(self.now, only_distance=True), "a week") + + def test_weeks(self): + + later = self.now.shift(weeks=2) + + self.assertEqual(self.now.humanize(later), "2 weeks ago") + self.assertEqual(later.humanize(self.now), "in 2 weeks") + + self.assertEqual(self.now.humanize(later, only_distance=True), "2 weeks") + self.assertEqual(later.humanize(self.now, only_distance=True), "2 weeks") + + def test_month(self): + + later = self.now.shift(months=1) + + self.assertEqual(self.now.humanize(later), "a month ago") + self.assertEqual(later.humanize(self.now), "in a month") + + self.assertEqual(self.now.humanize(later, only_distance=True), "a month") + self.assertEqual(later.humanize(self.now, only_distance=True), "a month") + + def test_months(self): + + later = self.now.shift(months=2) + earlier = self.now.shift(months=-2) + + self.assertEqual(earlier.humanize(self.now), "2 months ago") + self.assertEqual(later.humanize(self.now), "in 2 months") + + self.assertEqual(self.now.humanize(later, only_distance=True), "2 months") + self.assertEqual(later.humanize(self.now, only_distance=True), "2 months") + + def test_year(self): + + later = self.now.shift(years=1) + + self.assertEqual(self.now.humanize(later), "a year ago") + self.assertEqual(later.humanize(self.now), "in a year") + + self.assertEqual(self.now.humanize(later, only_distance=True), "a year") + self.assertEqual(later.humanize(self.now, only_distance=True), "a year") + + def test_years(self): + + later = self.now.shift(years=2) + + self.assertEqual(self.now.humanize(later), "2 years ago") + self.assertEqual(later.humanize(self.now), "in 2 years") + + self.assertEqual(self.now.humanize(later, only_distance=True), "2 years") + self.assertEqual(later.humanize(self.now, only_distance=True), "2 years") + + arw = arrow.Arrow(2014, 7, 2) + + result = arw.humanize(self.datetime) + + self.assertEqual(result, "in 2 years") + + def test_arrow(self): + + arw = arrow.Arrow.fromdatetime(self.datetime) + + result = arw.humanize(arrow.Arrow.fromdatetime(self.datetime)) + + self.assertEqual(result, "just now") + + def test_datetime_tzinfo(self): + + arw = arrow.Arrow.fromdatetime(self.datetime) + + result = arw.humanize(self.datetime.replace(tzinfo=tz.tzutc())) + + self.assertEqual(result, "just now") + + def test_other(self): + + arw = arrow.Arrow.fromdatetime(self.datetime) + + with self.assertRaises(TypeError): + arw.humanize(object()) + + def test_invalid_locale(self): + + arw = arrow.Arrow.fromdatetime(self.datetime) + + with self.assertRaises(ValueError): + arw.humanize(locale="klingon") + + def test_none(self): + + arw = arrow.Arrow.utcnow() + + result = arw.humanize() + + self.assertEqual(result, "just now") + + result = arw.humanize(None) + + self.assertEqual(result, "just now") + + def test_untranslated_granularity(self): + + arw = arrow.Arrow.utcnow() + later = arw.shift(weeks=1) + + # simulate an untranslated timeframe key + with patch.dict("arrow.locales.EnglishLocale.timeframes"): + del arrow.locales.EnglishLocale.timeframes["week"] + + with self.assertRaises(ValueError): + arw.humanize(later, granularity="week") + + +class ArrowHumanizeTestsWithLocale(Chai): + def setUp(self): + super(ArrowHumanizeTestsWithLocale, self).setUp() + + self.datetime = datetime(2013, 1, 1) + + def test_now(self): + + arw = arrow.Arrow(2013, 1, 1, 0, 0, 0) + + result = arw.humanize(self.datetime, locale="ru") + + self.assertEqual(result, "сейчас") + + def test_seconds(self): + arw = arrow.Arrow(2013, 1, 1, 0, 0, 44) + + result = arw.humanize(self.datetime, locale="ru") + + self.assertEqual(result, "через 44 несколько секунд") + + def test_years(self): + + arw = arrow.Arrow(2011, 7, 2) + + result = arw.humanize(self.datetime, locale="ru") + + self.assertEqual(result, "2 года назад") + + +class ArrowIsBetweenTests(Chai): + def test_start_before_end(self): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) + result = target.is_between(start, end) + self.assertFalse(result) + + def test_exclusive_exclusive_bounds(self): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 27)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 10)) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 36)) + result = target.is_between(start, end, "()") + self.assertTrue(result) + result = target.is_between(start, end) + self.assertTrue(result) + + def test_exclusive_exclusive_bounds_same_date(self): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + result = target.is_between(start, end, "()") + self.assertFalse(result) + + def test_inclusive_exclusive_bounds(self): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 6)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 4)) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 6)) + result = target.is_between(start, end, "[)") + self.assertFalse(result) + + def test_exclusive_inclusive_bounds(self): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + result = target.is_between(start, end, "(]") + self.assertTrue(result) + + def test_inclusive_inclusive_bounds_same_date(self): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + result = target.is_between(start, end, "[]") + self.assertTrue(result) + + def test_type_error_exception(self): + with self.assertRaises(TypeError): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + start = datetime(2013, 5, 5) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) + target.is_between(start, end) + + with self.assertRaises(TypeError): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) + end = datetime(2013, 5, 8) + target.is_between(start, end) + + with self.assertRaises(TypeError): + target.is_between(None, None) + + def test_attribute_error_exception(self): + target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) + start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) + end = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) + with self.assertRaises(AttributeError): + target.is_between(start, end, "][") + with self.assertRaises(AttributeError): + target.is_between(start, end, "") + with self.assertRaises(AttributeError): + target.is_between(start, end, "]") + with self.assertRaises(AttributeError): + target.is_between(start, end, "[") + with self.assertRaises(AttributeError): + target.is_between(start, end, "hello") + + +class ArrowUtilTests(Chai): + def test_get_datetime(self): + + get_datetime = arrow.Arrow._get_datetime + + arw = arrow.Arrow.utcnow() + dt = datetime.utcnow() + timestamp = time.time() + + self.assertEqual(get_datetime(arw), arw.datetime) + self.assertEqual(get_datetime(dt), dt) + self.assertEqual( + get_datetime(timestamp), arrow.Arrow.utcfromtimestamp(timestamp).datetime + ) + + with self.assertRaises(ValueError) as raise_ctx: + get_datetime("abc") + self.assertFalse("{}" in str(raise_ctx.exception)) + + def test_get_tzinfo(self): + + get_tzinfo = arrow.Arrow._get_tzinfo + + with self.assertRaises(ValueError) as raise_ctx: + get_tzinfo("abc") + self.assertFalse("{}" in str(raise_ctx.exception)) + + def test_get_iteration_params(self): + + self.assertEqual( + arrow.Arrow._get_iteration_params("end", None), ("end", sys.maxsize) + ) + self.assertEqual( + arrow.Arrow._get_iteration_params(None, 100), (arrow.Arrow.max, 100) + ) + self.assertEqual(arrow.Arrow._get_iteration_params(100, 120), (100, 120)) + + with self.assertRaises(ValueError): + arrow.Arrow._get_iteration_params(None, None) From 338e132c6d6af8a6e723e1a049b874254eed9fb2 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 4 Jan 2020 14:28:46 -0500 Subject: [PATCH 368/649] Removed mock from arrow_tests --- tests/arrow_tests.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index e9d7cd72e..561b81973 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -12,7 +12,6 @@ import simplejson as json from dateutil import tz from dateutil.relativedelta import FR, MO, SA, SU, TH, TU, WE -from mock import patch from arrow import arrow, util @@ -1678,17 +1677,16 @@ def test_none(self): assert result == "just now" - def test_untranslated_granularity(self): + def test_untranslated_granularity(self, mocker): arw = arrow.Arrow.utcnow() later = arw.shift(weeks=1) # simulate an untranslated timeframe key - with patch.dict("arrow.locales.EnglishLocale.timeframes"): - del arrow.locales.EnglishLocale.timeframes["week"] - - with pytest.raises(ValueError): - arw.humanize(later, granularity="week") + mocker.patch.dict("arrow.locales.EnglishLocale.timeframes") + del arrow.locales.EnglishLocale.timeframes["week"] + with pytest.raises(ValueError): + arw.humanize(later, granularity="week") class TestArrowHumanizeTestsWithLocale: From e2c55d7abd1e3170afd46e6bfe2e16cd38c90577 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 4 Jan 2020 14:33:15 -0500 Subject: [PATCH 369/649] Moved factory to pytest --- tests/factory_tests.py | 158 +++++++-------- tests/factory_tests.py.bak | 383 +++++++++++++++++++++++++++++++++++++ 2 files changed, 449 insertions(+), 92 deletions(-) create mode 100644 tests/factory_tests.py.bak diff --git a/tests/factory_tests.py b/tests/factory_tests.py index abdd0d029..840e9651c 100644 --- a/tests/factory_tests.py +++ b/tests/factory_tests.py @@ -3,23 +3,22 @@ from datetime import date, datetime import dateparser -from chai import Chai from dateutil import tz from arrow import factory, util from arrow.parser import ParserError +import pytest def assertDtEqual(dt1, dt2, within=10): - assertEqual(dt1.tzinfo, dt2.tzinfo) # noqa: F821 - assertTrue(abs(util.total_seconds(dt1 - dt2)) < within) # noqa: F821 + assert dt1.tzinfo == dt2.tzinfo + assert abs(util.total_seconds(dt1 - dt2)) < within -class GetTests(Chai): - def setUp(self): - super(GetTests, self).setUp() - - self.factory = factory.ArrowFactory() +class TestGet: + @classmethod + def setup_class(cls): + cls.factory = factory.ArrowFactory() def test_no_args(self): @@ -30,7 +29,7 @@ def test_timestamp_one_arg_no_arg(self): no_arg = self.factory.get(1406430900).timestamp one_arg = self.factory.get("1406430900", "X").timestamp - self.assertEqual(no_arg, one_arg) + assert no_arg == one_arg def test_one_arg_none(self): @@ -52,9 +51,9 @@ def test_one_arg_timestamp(self): tzinfo=tz.tzutc() ) - self.assertEqual(self.factory.get(int_timestamp), timestamp_dt) + assert self.factory.get(int_timestamp) == timestamp_dt - with self.assertRaises(ParserError): + with pytest.raises(ParserError): self.factory.get(str(int_timestamp)) float_timestamp = time.time() @@ -62,15 +61,15 @@ def test_one_arg_timestamp(self): tzinfo=tz.tzutc() ) - self.assertEqual(self.factory.get(float_timestamp), timestamp_dt) + assert self.factory.get(float_timestamp) == timestamp_dt - with self.assertRaises(ParserError): + with pytest.raises(ParserError): self.factory.get(str(float_timestamp)) # Regression test for issue #216 # Python 3 raises OverflowError, Python 2 raises ValueError timestamp = 99999999999999999999999999.99999999999999999999999999 - with self.assertRaises((OverflowError, ValueError)): + with pytest.raises((OverflowError, ValueError)): self.factory.get(timestamp) def test_one_arg_timestamp_with_tzinfo(self): @@ -88,20 +87,20 @@ def test_one_arg_arrow(self): arw = self.factory.utcnow() result = self.factory.get(arw) - self.assertEqual(arw, result) + assert arw == result def test_one_arg_datetime(self): dt = datetime.utcnow().replace(tzinfo=tz.tzutc()) - self.assertEqual(self.factory.get(dt), dt) + 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()) - self.assertEqual(self.factory.get(d), dt) + assert self.factory.get(d) == dt def test_one_arg_tzinfo(self): @@ -119,7 +118,7 @@ def test_one_arg_dateparser_datetime(self): # dateparser outputs: datetime.datetime(1990, 1, 1, 0, 0, tzinfo=) parsed_date = dateparser.parse("1990-01-01T00:00:00+00:00") dt_output = self.factory.get(parsed_date)._datetime.replace(tzinfo=tz.tzutc()) - self.assertEqual(dt_output, expected) + assert dt_output == expected def test_kwarg_tzinfo(self): @@ -141,7 +140,7 @@ def test_kwarg_tzinfo_string(self): assertDtEqual(self.factory.get(tzinfo="US/Pacific"), self.expected) - with self.assertRaises(ParserError): + with pytest.raises(ParserError): self.factory.get(tzinfo="US/PacificInvalidTzinfo") def test_one_arg_iso_str(self): @@ -165,80 +164,72 @@ def test_one_arg_iso_calendar(self): for pair in pairs: dt, iso = pair - self.assertEqual(self.factory.get(iso), self.factory.get(dt)) + assert self.factory.get(iso) == self.factory.get(dt) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.factory.get((2014, 7, 1, 4)) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.factory.get((2014, 7)) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): self.factory.get((2014, 70, 1)) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): self.factory.get((2014, 7, 10)) def test_one_arg_other(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.factory.get(object()) def test_one_arg_bool(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.factory.get(False) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.factory.get(True) def test_two_args_datetime_tzinfo(self): result = self.factory.get(datetime(2013, 1, 1), tz.gettz("US/Pacific")) - self.assertEqual( - result._datetime, datetime(2013, 1, 1, tzinfo=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") - self.assertEqual( - result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz("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")) - self.assertEqual( - result._datetime, datetime(2013, 1, 1, tzinfo=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") - self.assertEqual( - result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) - ) + assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) def test_two_args_datetime_other(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.factory.get(datetime.utcnow(), object()) def test_two_args_date_other(self): - with self.assertRaises(TypeError): + 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") - self.assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) def test_two_args_str_tzinfo(self): @@ -254,25 +245,23 @@ def test_two_args_twitter_format(self): twitter_date = "Fri Apr 08 21:08:54 +0000 2016" result = self.factory.get(twitter_date, "ddd MMM DD HH:mm:ss Z YYYY") - self.assertEqual( - result._datetime, datetime(2016, 4, 8, 21, 8, 54, tzinfo=tz.tzutc()) - ) + 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"]) - self.assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) def test_two_args_unicode_unicode(self): result = self.factory.get(u"2013-01-01", u"YYYY-MM-DD") - self.assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) def test_two_args_other(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.factory.get(object(), object()) def test_three_args_with_tzinfo(self): @@ -280,81 +269,67 @@ def test_three_args_with_tzinfo(self): timefmt = "YYYYMMDD" d = "20150514" - self.assertEqual( - self.factory.get(d, timefmt, tzinfo=tz.tzlocal()), - datetime(2015, 5, 14, tzinfo=tz.tzlocal()), + assert self.factory.get(d, timefmt, tzinfo=tz.tzlocal()) == datetime( + 2015, 5, 14, tzinfo=tz.tzlocal() ) def test_three_args(self): - self.assertEqual( - self.factory.get(2013, 1, 1), datetime(2013, 1, 1, tzinfo=tz.tzutc()) - ) + assert self.factory.get(2013, 1, 1) == datetime(2013, 1, 1, tzinfo=tz.tzutc()) def test_full_kwargs(self): - self.assertEqual( - 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): - self.assertEqual( - self.factory.get(year=2016, month=7, day=14), - datetime(2016, 7, 14, 0, 0, tzinfo=tz.tzutc()), + assert self.factory.get(year=2016, month=7, day=14) == datetime( + 2016, 7, 14, 0, 0, tzinfo=tz.tzutc() ) def test_tzinfo_string_kwargs(self): result = self.factory.get("2019072807", "YYYYMMDDHH", tzinfo="UTC") - self.assertEqual( - result._datetime, datetime(2019, 7, 28, 7, 0, 0, 0, tzinfo=tz.tzutc()) - ) + assert result._datetime == datetime(2019, 7, 28, 7, 0, 0, 0, tzinfo=tz.tzutc()) def test_insufficient_kwargs(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.factory.get(year=2016) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.factory.get(year=2016, month=7) def test_locale(self): result = self.factory.get("2010", "YYYY", locale="ja") - self.assertEqual( - result._datetime, datetime(2010, 1, 1, 0, 0, 0, 0, tzinfo=tz.tzutc()) - ) + assert result._datetime == datetime(2010, 1, 1, 0, 0, 0, 0, tzinfo=tz.tzutc()) # regression test for issue #701 result = self.factory.get( "Montag, 9. September 2019, 16:15-20:00", "dddd, D. MMMM YYYY", locale="de" ) - self.assertEqual( - result._datetime, datetime(2019, 9, 9, 0, 0, 0, 0, tzinfo=tz.tzutc()) - ) + assert result._datetime == datetime(2019, 9, 9, 0, 0, 0, 0, tzinfo=tz.tzutc()) def test_locale_kwarg_only(self): res = self.factory.get(locale="ja") - self.assertEqual(res.tzinfo, tz.tzutc()) + assert res.tzinfo == tz.tzutc() def test_locale_with_tzinfo(self): res = self.factory.get(locale="ja", tzinfo=tz.gettz("Asia/Tokyo")) - self.assertEqual(res.tzinfo, tz.gettz("Asia/Tokyo")) + assert res.tzinfo == tz.gettz("Asia/Tokyo") -class UtcNowTests(Chai): - def setUp(self): - super(UtcNowTests, self).setUp() - - self.factory = factory.ArrowFactory() +class TestUtcNow: + @classmethod + def setup_class(cls): + cls.factory = factory.ArrowFactory() def test_utcnow(self): @@ -364,11 +339,10 @@ def test_utcnow(self): ) -class NowTests(Chai): - def setUp(self): - super(NowTests, self).setUp() - - self.factory = factory.ArrowFactory() +class TestNow: + @classmethod + def setup_class(cls): + cls.factory = factory.ArrowFactory() def test_no_tz(self): diff --git a/tests/factory_tests.py.bak b/tests/factory_tests.py.bak new file mode 100644 index 000000000..abdd0d029 --- /dev/null +++ b/tests/factory_tests.py.bak @@ -0,0 +1,383 @@ +# -*- coding: utf-8 -*- +import time +from datetime import date, datetime + +import dateparser +from chai import Chai +from dateutil import tz + +from arrow import factory, util +from arrow.parser import ParserError + + +def assertDtEqual(dt1, dt2, within=10): + assertEqual(dt1.tzinfo, dt2.tzinfo) # noqa: F821 + assertTrue(abs(util.total_seconds(dt1 - dt2)) < within) # noqa: F821 + + +class GetTests(Chai): + def setUp(self): + super(GetTests, self).setUp() + + self.factory = factory.ArrowFactory() + + def test_no_args(self): + + assertDtEqual(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 + + self.assertEqual(no_arg, one_arg) + + def test_one_arg_none(self): + + assertDtEqual( + self.factory.get(None), datetime.utcnow().replace(tzinfo=tz.tzutc()) + ) + + def test_struct_time(self): + + assertDtEqual( + 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() + ) + + self.assertEqual(self.factory.get(int_timestamp), timestamp_dt) + + with self.assertRaises(ParserError): + self.factory.get(str(int_timestamp)) + + float_timestamp = time.time() + timestamp_dt = datetime.utcfromtimestamp(float_timestamp).replace( + tzinfo=tz.tzutc() + ) + + self.assertEqual(self.factory.get(float_timestamp), timestamp_dt) + + with self.assertRaises(ParserError): + self.factory.get(str(float_timestamp)) + + # Regression test for issue #216 + # Python 3 raises OverflowError, Python 2 raises ValueError + timestamp = 99999999999999999999999999.99999999999999999999999999 + with self.assertRaises((OverflowError, ValueError)): + self.factory.get(timestamp) + + def test_one_arg_timestamp_with_tzinfo(self): + + timestamp = time.time() + timestamp_dt = datetime.fromtimestamp(timestamp, tz=tz.tzutc()).astimezone( + tz.gettz("US/Pacific") + ) + timezone = tz.gettz("US/Pacific") + + assertDtEqual(self.factory.get(timestamp, tzinfo=timezone), timestamp_dt) + + def test_one_arg_arrow(self): + + arw = self.factory.utcnow() + result = self.factory.get(arw) + + self.assertEqual(arw, result) + + def test_one_arg_datetime(self): + + dt = datetime.utcnow().replace(tzinfo=tz.tzutc()) + + self.assertEqual(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()) + + self.assertEqual(self.factory.get(d), dt) + + def test_one_arg_tzinfo(self): + + self.expected = ( + datetime.utcnow() + .replace(tzinfo=tz.tzutc()) + .astimezone(tz.gettz("US/Pacific")) + ) + + assertDtEqual(self.factory.get(tz.gettz("US/Pacific")), self.expected) + + # regression test for issue #658 + def test_one_arg_dateparser_datetime(self): + expected = datetime(1990, 1, 1).replace(tzinfo=tz.tzutc()) + # dateparser outputs: datetime.datetime(1990, 1, 1, 0, 0, tzinfo=) + parsed_date = dateparser.parse("1990-01-01T00:00:00+00:00") + dt_output = self.factory.get(parsed_date)._datetime.replace(tzinfo=tz.tzutc()) + self.assertEqual(dt_output, expected) + + def test_kwarg_tzinfo(self): + + self.expected = ( + datetime.utcnow() + .replace(tzinfo=tz.tzutc()) + .astimezone(tz.gettz("US/Pacific")) + ) + + assertDtEqual(self.factory.get(tzinfo=tz.gettz("US/Pacific")), self.expected) + + def test_kwarg_tzinfo_string(self): + + self.expected = ( + datetime.utcnow() + .replace(tzinfo=tz.tzutc()) + .astimezone(tz.gettz("US/Pacific")) + ) + + assertDtEqual(self.factory.get(tzinfo="US/Pacific"), self.expected) + + with self.assertRaises(ParserError): + self.factory.get(tzinfo="US/PacificInvalidTzinfo") + + def test_one_arg_iso_str(self): + + dt = datetime.utcnow() + + assertDtEqual(self.factory.get(dt.isoformat()), dt.replace(tzinfo=tz.tzutc())) + + def test_one_arg_iso_calendar(self): + + pairs = [ + (datetime(2004, 1, 4), (2004, 1, 7)), + (datetime(2008, 12, 30), (2009, 1, 2)), + (datetime(2010, 1, 2), (2009, 53, 6)), + (datetime(2000, 2, 29), (2000, 9, 2)), + (datetime(2005, 1, 1), (2004, 53, 6)), + (datetime(2010, 1, 4), (2010, 1, 1)), + (datetime(2010, 1, 3), (2009, 53, 7)), + (datetime(2003, 12, 29), (2004, 1, 1)), + ] + + for pair in pairs: + dt, iso = pair + self.assertEqual(self.factory.get(iso), self.factory.get(dt)) + + with self.assertRaises(TypeError): + self.factory.get((2014, 7, 1, 4)) + + with self.assertRaises(TypeError): + self.factory.get((2014, 7)) + + with self.assertRaises(ValueError): + self.factory.get((2014, 70, 1)) + + with self.assertRaises(ValueError): + self.factory.get((2014, 7, 10)) + + def test_one_arg_other(self): + + with self.assertRaises(TypeError): + self.factory.get(object()) + + def test_one_arg_bool(self): + + with self.assertRaises(TypeError): + self.factory.get(False) + + with self.assertRaises(TypeError): + self.factory.get(True) + + def test_two_args_datetime_tzinfo(self): + + result = self.factory.get(datetime(2013, 1, 1), tz.gettz("US/Pacific")) + + self.assertEqual( + 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") + + self.assertEqual( + 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")) + + self.assertEqual( + 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") + + self.assertEqual( + result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) + ) + + def test_two_args_datetime_other(self): + + with self.assertRaises(TypeError): + self.factory.get(datetime.utcnow(), object()) + + def test_two_args_date_other(self): + + with self.assertRaises(TypeError): + self.factory.get(date.today(), object()) + + def test_two_args_str_str(self): + + result = self.factory.get("2013-01-01", "YYYY-MM-DD") + + self.assertEqual(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")) + + assertDtEqual( + result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) + ) + + 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") + + self.assertEqual( + 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"]) + + self.assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + + def test_two_args_unicode_unicode(self): + + result = self.factory.get(u"2013-01-01", u"YYYY-MM-DD") + + self.assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.tzutc())) + + def test_two_args_other(self): + + with self.assertRaises(TypeError): + self.factory.get(object(), object()) + + def test_three_args_with_tzinfo(self): + + timefmt = "YYYYMMDD" + d = "20150514" + + self.assertEqual( + self.factory.get(d, timefmt, tzinfo=tz.tzlocal()), + datetime(2015, 5, 14, tzinfo=tz.tzlocal()), + ) + + def test_three_args(self): + + self.assertEqual( + self.factory.get(2013, 1, 1), datetime(2013, 1, 1, tzinfo=tz.tzutc()) + ) + + def test_full_kwargs(self): + + self.assertEqual( + 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): + + self.assertEqual( + self.factory.get(year=2016, month=7, day=14), + datetime(2016, 7, 14, 0, 0, tzinfo=tz.tzutc()), + ) + + def test_tzinfo_string_kwargs(self): + result = self.factory.get("2019072807", "YYYYMMDDHH", tzinfo="UTC") + self.assertEqual( + result._datetime, datetime(2019, 7, 28, 7, 0, 0, 0, tzinfo=tz.tzutc()) + ) + + def test_insufficient_kwargs(self): + + with self.assertRaises(TypeError): + self.factory.get(year=2016) + + with self.assertRaises(TypeError): + self.factory.get(year=2016, month=7) + + def test_locale(self): + result = self.factory.get("2010", "YYYY", locale="ja") + self.assertEqual( + result._datetime, datetime(2010, 1, 1, 0, 0, 0, 0, tzinfo=tz.tzutc()) + ) + + # regression test for issue #701 + result = self.factory.get( + "Montag, 9. September 2019, 16:15-20:00", "dddd, D. MMMM YYYY", locale="de" + ) + self.assertEqual( + result._datetime, datetime(2019, 9, 9, 0, 0, 0, 0, tzinfo=tz.tzutc()) + ) + + def test_locale_kwarg_only(self): + res = self.factory.get(locale="ja") + self.assertEqual(res.tzinfo, tz.tzutc()) + + def test_locale_with_tzinfo(self): + res = self.factory.get(locale="ja", tzinfo=tz.gettz("Asia/Tokyo")) + self.assertEqual(res.tzinfo, tz.gettz("Asia/Tokyo")) + + +class UtcNowTests(Chai): + def setUp(self): + super(UtcNowTests, self).setUp() + + self.factory = factory.ArrowFactory() + + def test_utcnow(self): + + assertDtEqual( + self.factory.utcnow()._datetime, + datetime.utcnow().replace(tzinfo=tz.tzutc()), + ) + + +class NowTests(Chai): + def setUp(self): + super(NowTests, self).setUp() + + self.factory = factory.ArrowFactory() + + def test_no_tz(self): + + assertDtEqual(self.factory.now(), datetime.now(tz.tzlocal())) + + def test_tzinfo(self): + + assertDtEqual(self.factory.now(tz.gettz("EST")), datetime.now(tz.gettz("EST"))) + + def test_tz_str(self): + + assertDtEqual(self.factory.now("EST"), datetime.now(tz.gettz("EST"))) From ef43261208bce0e42a4e006b3acc1366e32bbce5 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 4 Jan 2020 14:35:05 -0500 Subject: [PATCH 370/649] Moved formatter tests to pytest --- tests/formatter_tests.py | 150 +++++++++++++------------- tests/formatter_tests.py.bak | 199 +++++++++++++++++++++++++++++++++++ 2 files changed, 273 insertions(+), 76 deletions(-) create mode 100644 tests/formatter_tests.py.bak diff --git a/tests/formatter_tests.py b/tests/formatter_tests.py index 848ac260a..33accf755 100644 --- a/tests/formatter_tests.py +++ b/tests/formatter_tests.py @@ -3,7 +3,6 @@ from datetime import datetime import pytz -from chai import Chai from dateutil import tz as dateutil_tz from arrow import formatter @@ -11,11 +10,10 @@ from .utils import make_full_tz_list -class DateTimeFormatterFormatTokenTests(Chai): - def setUp(self): - super(DateTimeFormatterFormatTokenTests, self).setUp() - - self.formatter = formatter.DateTimeFormatter() +class TestDateTimeFormatterFormatToken: + @classmethod + def setup_class(cls): + cls.formatter = formatter.DateTimeFormatter() def test_format(self): @@ -23,103 +21,103 @@ def test_format(self): result = self.formatter.format(dt, "MM-DD-YYYY hh:mm:ss a") - self.assertEqual(result, "02-05-2013 12:32:51 pm") + assert result == "02-05-2013 12:32:51 pm" def test_year(self): dt = datetime(2013, 1, 1) - self.assertEqual(self.formatter._format_token(dt, "YYYY"), "2013") - self.assertEqual(self.formatter._format_token(dt, "YY"), "13") + 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) - self.assertEqual(self.formatter._format_token(dt, "MMMM"), "January") - self.assertEqual(self.formatter._format_token(dt, "MMM"), "Jan") - self.assertEqual(self.formatter._format_token(dt, "MM"), "01") - self.assertEqual(self.formatter._format_token(dt, "M"), "1") + assert self.formatter._format_token(dt, "MMMM") == "January" + assert self.formatter._format_token(dt, "MMM") == "Jan" + assert self.formatter._format_token(dt, "MM") == "01" + assert self.formatter._format_token(dt, "M") == "1" def test_day(self): dt = datetime(2013, 2, 1) - self.assertEqual(self.formatter._format_token(dt, "DDDD"), "032") - self.assertEqual(self.formatter._format_token(dt, "DDD"), "32") - self.assertEqual(self.formatter._format_token(dt, "DD"), "01") - self.assertEqual(self.formatter._format_token(dt, "D"), "1") - self.assertEqual(self.formatter._format_token(dt, "Do"), "1st") + assert self.formatter._format_token(dt, "DDDD") == "032" + assert self.formatter._format_token(dt, "DDD") == "32" + assert self.formatter._format_token(dt, "DD") == "01" + assert self.formatter._format_token(dt, "D") == "1" + assert self.formatter._format_token(dt, "Do") == "1st" - self.assertEqual(self.formatter._format_token(dt, "dddd"), "Friday") - self.assertEqual(self.formatter._format_token(dt, "ddd"), "Fri") - self.assertEqual(self.formatter._format_token(dt, "d"), "5") + assert self.formatter._format_token(dt, "dddd") == "Friday" + assert self.formatter._format_token(dt, "ddd") == "Fri" + assert self.formatter._format_token(dt, "d") == "5" def test_hour(self): dt = datetime(2013, 1, 1, 2) - self.assertEqual(self.formatter._format_token(dt, "HH"), "02") - self.assertEqual(self.formatter._format_token(dt, "H"), "2") + assert self.formatter._format_token(dt, "HH") == "02" + assert self.formatter._format_token(dt, "H") == "2" dt = datetime(2013, 1, 1, 13) - self.assertEqual(self.formatter._format_token(dt, "HH"), "13") - self.assertEqual(self.formatter._format_token(dt, "H"), "13") + assert self.formatter._format_token(dt, "HH") == "13" + assert self.formatter._format_token(dt, "H") == "13" dt = datetime(2013, 1, 1, 2) - self.assertEqual(self.formatter._format_token(dt, "hh"), "02") - self.assertEqual(self.formatter._format_token(dt, "h"), "2") + assert self.formatter._format_token(dt, "hh") == "02" + assert self.formatter._format_token(dt, "h") == "2" dt = datetime(2013, 1, 1, 13) - self.assertEqual(self.formatter._format_token(dt, "hh"), "01") - self.assertEqual(self.formatter._format_token(dt, "h"), "1") + assert self.formatter._format_token(dt, "hh") == "01" + assert self.formatter._format_token(dt, "h") == "1" # test that 12-hour time converts to '12' at midnight dt = datetime(2013, 1, 1, 0) - self.assertEqual(self.formatter._format_token(dt, "hh"), "12") - self.assertEqual(self.formatter._format_token(dt, "h"), "12") + assert self.formatter._format_token(dt, "hh") == "12" + assert self.formatter._format_token(dt, "h") == "12" def test_minute(self): dt = datetime(2013, 1, 1, 0, 1) - self.assertEqual(self.formatter._format_token(dt, "mm"), "01") - self.assertEqual(self.formatter._format_token(dt, "m"), "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) - self.assertEqual(self.formatter._format_token(dt, "ss"), "01") - self.assertEqual(self.formatter._format_token(dt, "s"), "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) - self.assertEqual(self.formatter._format_token(dt, "SSSSSS"), "123456") - self.assertEqual(self.formatter._format_token(dt, "SSSSS"), "12345") - self.assertEqual(self.formatter._format_token(dt, "SSSS"), "1234") - self.assertEqual(self.formatter._format_token(dt, "SSS"), "123") - self.assertEqual(self.formatter._format_token(dt, "SS"), "12") - self.assertEqual(self.formatter._format_token(dt, "S"), "1") + assert self.formatter._format_token(dt, "SSSSSS") == "123456" + assert self.formatter._format_token(dt, "SSSSS") == "12345" + assert self.formatter._format_token(dt, "SSSS") == "1234" + assert self.formatter._format_token(dt, "SSS") == "123" + assert self.formatter._format_token(dt, "SS") == "12" + assert self.formatter._format_token(dt, "S") == "1" dt = datetime(2013, 1, 1, 0, 0, 0, 2000) - self.assertEqual(self.formatter._format_token(dt, "SSSSSS"), "002000") - self.assertEqual(self.formatter._format_token(dt, "SSSSS"), "00200") - self.assertEqual(self.formatter._format_token(dt, "SSSS"), "0020") - self.assertEqual(self.formatter._format_token(dt, "SSS"), "002") - self.assertEqual(self.formatter._format_token(dt, "SS"), "00") - self.assertEqual(self.formatter._format_token(dt, "S"), "0") + assert self.formatter._format_token(dt, "SSSSSS") == "002000" + assert self.formatter._format_token(dt, "SSSSS") == "00200" + assert self.formatter._format_token(dt, "SSSS") == "0020" + assert self.formatter._format_token(dt, "SSS") == "002" + assert self.formatter._format_token(dt, "SS") == "00" + assert self.formatter._format_token(dt, "S") == "0" def test_timestamp(self): timestamp = time.time() dt = datetime.utcfromtimestamp(timestamp) - self.assertEqual(self.formatter._format_token(dt, "X"), str(int(timestamp))) + assert self.formatter._format_token(dt, "X") == str(int(timestamp)) def test_timezone(self): dt = datetime.utcnow().replace(tzinfo=dateutil_tz.gettz("US/Pacific")) result = self.formatter._format_token(dt, "ZZ") - self.assertTrue(result == "-07:00" or result == "-08:00") + assert result == "-07:00" or result == "-08:00" result = self.formatter._format_token(dt, "Z") - self.assertTrue(result == "-0700" or result == "-0800") + assert result == "-0700" or result == "-0800" def test_timezone_formatter(self): @@ -131,69 +129,69 @@ def test_timezone_formatter(self): abbreviation = dt.tzname() result = self.formatter._format_token(dt, "ZZZ") - self.assertEqual(result, abbreviation) + assert result == abbreviation def test_am_pm(self): dt = datetime(2012, 1, 1, 11) - self.assertEqual(self.formatter._format_token(dt, "a"), "am") - self.assertEqual(self.formatter._format_token(dt, "A"), "AM") + assert self.formatter._format_token(dt, "a") == "am" + assert self.formatter._format_token(dt, "A") == "AM" dt = datetime(2012, 1, 1, 13) - self.assertEqual(self.formatter._format_token(dt, "a"), "pm") - self.assertEqual(self.formatter._format_token(dt, "A"), "PM") + assert self.formatter._format_token(dt, "a") == "pm" + assert self.formatter._format_token(dt, "A") == "PM" def test_nonsense(self): dt = datetime(2012, 1, 1, 11) - self.assertEqual(self.formatter._format_token(dt, None), None) - self.assertEqual(self.formatter._format_token(dt, "NONSENSE"), None) + assert self.formatter._format_token(dt, None) == None + assert self.formatter._format_token(dt, "NONSENSE") == None def test_escape(self): - self.assertEqual( + assert ( self.formatter.format( datetime(2015, 12, 10, 17, 9), "MMMM D, YYYY [at] h:mma" - ), - "December 10, 2015 at 5:09pm", + ) + == "December 10, 2015 at 5:09pm" ) - self.assertEqual( + assert ( self.formatter.format( datetime(2015, 12, 10, 17, 9), "[MMMM] M D, YYYY [at] h:mma" - ), - "MMMM 12 10, 2015 at 5:09pm", + ) + == "MMMM 12 10, 2015 at 5:09pm" ) - self.assertEqual( + assert ( self.formatter.format( datetime(1990, 11, 25), "[It happened on] MMMM Do [in the year] YYYY [a long time ago]", - ), - "It happened on November 25th in the year 1990 a long time ago", + ) + == "It happened on November 25th in the year 1990 a long time ago" ) - self.assertEqual( + assert ( self.formatter.format( datetime(1990, 11, 25), "[It happened on] MMMM Do [in the][ year] YYYY [a long time ago]", - ), - "It happened on November 25th in the year 1990 a long time ago", + ) + == "It happened on November 25th in the year 1990 a long time ago" ) - self.assertEqual( + assert ( self.formatter.format( datetime(1, 1, 1), "[I'm][ entirely][ escaped,][ weee!]" - ), - "I'm entirely escaped, weee!", + ) + == "I'm entirely escaped, weee!" ) # Special RegEx characters - self.assertEqual( + assert ( self.formatter.format( datetime(2017, 12, 31, 2, 0), "MMM DD, YYYY |^${}().*+?<>-& h:mm A" - ), - "Dec 31, 2017 |^${}().*+?<>-& 2:00 AM", + ) + == "Dec 31, 2017 |^${}().*+?<>-& 2:00 AM" ) # Escaping is atomic: brackets inside brackets are treated litterally - self.assertEqual(self.formatter.format(datetime(1, 1, 1), "[[[ ]]"), "[[ ]") + assert self.formatter.format(datetime(1, 1, 1), "[[[ ]]") == "[[ ]" diff --git a/tests/formatter_tests.py.bak b/tests/formatter_tests.py.bak new file mode 100644 index 000000000..848ac260a --- /dev/null +++ b/tests/formatter_tests.py.bak @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +import time +from datetime import datetime + +import pytz +from chai import Chai +from dateutil import tz as dateutil_tz + +from arrow import formatter + +from .utils import make_full_tz_list + + +class DateTimeFormatterFormatTokenTests(Chai): + def setUp(self): + super(DateTimeFormatterFormatTokenTests, self).setUp() + + self.formatter = formatter.DateTimeFormatter() + + def test_format(self): + + dt = datetime(2013, 2, 5, 12, 32, 51) + + result = self.formatter.format(dt, "MM-DD-YYYY hh:mm:ss a") + + self.assertEqual(result, "02-05-2013 12:32:51 pm") + + def test_year(self): + + dt = datetime(2013, 1, 1) + self.assertEqual(self.formatter._format_token(dt, "YYYY"), "2013") + self.assertEqual(self.formatter._format_token(dt, "YY"), "13") + + def test_month(self): + + dt = datetime(2013, 1, 1) + self.assertEqual(self.formatter._format_token(dt, "MMMM"), "January") + self.assertEqual(self.formatter._format_token(dt, "MMM"), "Jan") + self.assertEqual(self.formatter._format_token(dt, "MM"), "01") + self.assertEqual(self.formatter._format_token(dt, "M"), "1") + + def test_day(self): + + dt = datetime(2013, 2, 1) + self.assertEqual(self.formatter._format_token(dt, "DDDD"), "032") + self.assertEqual(self.formatter._format_token(dt, "DDD"), "32") + self.assertEqual(self.formatter._format_token(dt, "DD"), "01") + self.assertEqual(self.formatter._format_token(dt, "D"), "1") + self.assertEqual(self.formatter._format_token(dt, "Do"), "1st") + + self.assertEqual(self.formatter._format_token(dt, "dddd"), "Friday") + self.assertEqual(self.formatter._format_token(dt, "ddd"), "Fri") + self.assertEqual(self.formatter._format_token(dt, "d"), "5") + + def test_hour(self): + + dt = datetime(2013, 1, 1, 2) + self.assertEqual(self.formatter._format_token(dt, "HH"), "02") + self.assertEqual(self.formatter._format_token(dt, "H"), "2") + + dt = datetime(2013, 1, 1, 13) + self.assertEqual(self.formatter._format_token(dt, "HH"), "13") + self.assertEqual(self.formatter._format_token(dt, "H"), "13") + + dt = datetime(2013, 1, 1, 2) + self.assertEqual(self.formatter._format_token(dt, "hh"), "02") + self.assertEqual(self.formatter._format_token(dt, "h"), "2") + + dt = datetime(2013, 1, 1, 13) + self.assertEqual(self.formatter._format_token(dt, "hh"), "01") + self.assertEqual(self.formatter._format_token(dt, "h"), "1") + + # test that 12-hour time converts to '12' at midnight + dt = datetime(2013, 1, 1, 0) + self.assertEqual(self.formatter._format_token(dt, "hh"), "12") + self.assertEqual(self.formatter._format_token(dt, "h"), "12") + + def test_minute(self): + + dt = datetime(2013, 1, 1, 0, 1) + self.assertEqual(self.formatter._format_token(dt, "mm"), "01") + self.assertEqual(self.formatter._format_token(dt, "m"), "1") + + def test_second(self): + + dt = datetime(2013, 1, 1, 0, 0, 1) + self.assertEqual(self.formatter._format_token(dt, "ss"), "01") + self.assertEqual(self.formatter._format_token(dt, "s"), "1") + + def test_sub_second(self): + + dt = datetime(2013, 1, 1, 0, 0, 0, 123456) + self.assertEqual(self.formatter._format_token(dt, "SSSSSS"), "123456") + self.assertEqual(self.formatter._format_token(dt, "SSSSS"), "12345") + self.assertEqual(self.formatter._format_token(dt, "SSSS"), "1234") + self.assertEqual(self.formatter._format_token(dt, "SSS"), "123") + self.assertEqual(self.formatter._format_token(dt, "SS"), "12") + self.assertEqual(self.formatter._format_token(dt, "S"), "1") + + dt = datetime(2013, 1, 1, 0, 0, 0, 2000) + self.assertEqual(self.formatter._format_token(dt, "SSSSSS"), "002000") + self.assertEqual(self.formatter._format_token(dt, "SSSSS"), "00200") + self.assertEqual(self.formatter._format_token(dt, "SSSS"), "0020") + self.assertEqual(self.formatter._format_token(dt, "SSS"), "002") + self.assertEqual(self.formatter._format_token(dt, "SS"), "00") + self.assertEqual(self.formatter._format_token(dt, "S"), "0") + + def test_timestamp(self): + + timestamp = time.time() + dt = datetime.utcfromtimestamp(timestamp) + self.assertEqual(self.formatter._format_token(dt, "X"), str(int(timestamp))) + + def test_timezone(self): + + dt = datetime.utcnow().replace(tzinfo=dateutil_tz.gettz("US/Pacific")) + + result = self.formatter._format_token(dt, "ZZ") + self.assertTrue(result == "-07:00" or result == "-08:00") + + result = self.formatter._format_token(dt, "Z") + self.assertTrue(result == "-0700" or result == "-0800") + + def test_timezone_formatter(self): + + for full_name in make_full_tz_list(): + # 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_name) + ) + abbreviation = dt.tzname() + + result = self.formatter._format_token(dt, "ZZZ") + self.assertEqual(result, abbreviation) + + def test_am_pm(self): + + dt = datetime(2012, 1, 1, 11) + self.assertEqual(self.formatter._format_token(dt, "a"), "am") + self.assertEqual(self.formatter._format_token(dt, "A"), "AM") + + dt = datetime(2012, 1, 1, 13) + self.assertEqual(self.formatter._format_token(dt, "a"), "pm") + self.assertEqual(self.formatter._format_token(dt, "A"), "PM") + + def test_nonsense(self): + dt = datetime(2012, 1, 1, 11) + self.assertEqual(self.formatter._format_token(dt, None), None) + self.assertEqual(self.formatter._format_token(dt, "NONSENSE"), None) + + def test_escape(self): + + self.assertEqual( + self.formatter.format( + datetime(2015, 12, 10, 17, 9), "MMMM D, YYYY [at] h:mma" + ), + "December 10, 2015 at 5:09pm", + ) + + self.assertEqual( + self.formatter.format( + datetime(2015, 12, 10, 17, 9), "[MMMM] M D, YYYY [at] h:mma" + ), + "MMMM 12 10, 2015 at 5:09pm", + ) + + self.assertEqual( + self.formatter.format( + datetime(1990, 11, 25), + "[It happened on] MMMM Do [in the year] YYYY [a long time ago]", + ), + "It happened on November 25th in the year 1990 a long time ago", + ) + + self.assertEqual( + self.formatter.format( + datetime(1990, 11, 25), + "[It happened on] MMMM Do [in the][ year] YYYY [a long time ago]", + ), + "It happened on November 25th in the year 1990 a long time ago", + ) + + self.assertEqual( + self.formatter.format( + datetime(1, 1, 1), "[I'm][ entirely][ escaped,][ weee!]" + ), + "I'm entirely escaped, weee!", + ) + + # Special RegEx characters + self.assertEqual( + self.formatter.format( + datetime(2017, 12, 31, 2, 0), "MMM DD, YYYY |^${}().*+?<>-& h:mm A" + ), + "Dec 31, 2017 |^${}().*+?<>-& 2:00 AM", + ) + + # Escaping is atomic: brackets inside brackets are treated litterally + self.assertEqual(self.formatter.format(datetime(1, 1, 1), "[[[ ]]"), "[[ ]") From db5ac7e6204c1316fc708d5287dca3539b1ee536 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 4 Jan 2020 14:43:18 -0500 Subject: [PATCH 371/649] Moved locales tests to pytest --- tests/formatter_tests.py | 4 +- tests/locales_tests.py | 1170 +++++++++++++++++------------------- tests/locales_tests.py.bak | 954 +++++++++++++++++++++++++++++ 3 files changed, 1507 insertions(+), 621 deletions(-) create mode 100644 tests/locales_tests.py.bak diff --git a/tests/formatter_tests.py b/tests/formatter_tests.py index 33accf755..dbf287364 100644 --- a/tests/formatter_tests.py +++ b/tests/formatter_tests.py @@ -143,8 +143,8 @@ def test_am_pm(self): def test_nonsense(self): dt = datetime(2012, 1, 1, 11) - assert self.formatter._format_token(dt, None) == None - assert self.formatter._format_token(dt, "NONSENSE") == None + assert self.formatter._format_token(dt, None) is None + assert self.formatter._format_token(dt, "NONSENSE") is None def test_escape(self): diff --git a/tests/locales_tests.py b/tests/locales_tests.py index af205b435..cd1e2e611 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -1,616 +1,586 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from chai import Chai - from arrow import arrow, locales +import pytest -class LocaleValidationTests(Chai): +class TestLocaleValidation: """Validate locales to ensure that translations are valid and complete""" - def setUp(self): - super(LocaleValidationTests, self).setUp() - - self.locales = locales._locales + @classmethod + def setup_class(cls): + cls.locales = locales._locales def test_locale_validation(self): for _, locale_cls in self.locales.items(): # 7 days + 1 spacer to allow for 1-indexing of months - self.assertEqual(len(locale_cls.day_names), 8) - self.assertTrue(locale_cls.day_names[0] == "") + assert len(locale_cls.day_names) == 8 + assert locale_cls.day_names[0] == "" # ensure that all string from index 1 onward are valid (not blank or None) - self.assertTrue(all(locale_cls.day_names[1:])) + assert all(locale_cls.day_names[1:]) - self.assertEqual(len(locale_cls.day_abbreviations), 8) - self.assertTrue(locale_cls.day_abbreviations[0] == "") - self.assertTrue(all(locale_cls.day_abbreviations[1:])) + assert len(locale_cls.day_abbreviations) == 8 + assert locale_cls.day_abbreviations[0] == "" + assert all(locale_cls.day_abbreviations[1:]) # 12 months + 1 spacer to allow for 1-indexing of months - self.assertEqual(len(locale_cls.month_names), 13) - self.assertTrue(locale_cls.month_names[0] == "") - self.assertTrue(all(locale_cls.month_names[1:])) + assert len(locale_cls.month_names) == 13 + assert locale_cls.month_names[0] == "" + assert all(locale_cls.month_names[1:]) - self.assertEqual(len(locale_cls.month_abbreviations), 13) - self.assertTrue(locale_cls.month_abbreviations[0] == "") - self.assertTrue(all(locale_cls.month_abbreviations[1:])) + assert len(locale_cls.month_abbreviations) == 13 + assert locale_cls.month_abbreviations[0] == "" + assert all(locale_cls.month_abbreviations[1:]) - self.assertTrue(len(locale_cls.names) > 0) - self.assertTrue(locale_cls.past is not None) - self.assertTrue(locale_cls.future is not None) + assert len(locale_cls.names) > 0 + assert locale_cls.past is not None + assert locale_cls.future is not None -class ModuleTests(Chai): - def test_get_locale(self): +class TestModule: + def test_get_locale(self, mocker): + mock_locale = mocker.Mock() + mock_locale_cls = mocker.Mock() + mock_locale_cls.return_value = mock_locale - mock_locales = self.mock(locales, "_locales") - mock_locale_cls = self.mock() - mock_locale = self.mock() + with pytest.raises(ValueError): + arrow.locales.get_locale("locale_name") - self.expect(mock_locales.get).args("name").returns(mock_locale_cls) - self.expect(mock_locale_cls).returns(mock_locale) + cls_dict = arrow.locales._locales + mocker.patch.dict(cls_dict, {"locale_name": mock_locale_cls}) - result = locales.get_locale("name") + result = arrow.locales.get_locale("locale_name") - self.assertEqual(result, mock_locale) + assert result == mock_locale def test_locales(self): - self.assertTrue(len(locales._locales) > 0) + assert len(locales._locales) > 0 -class LocaleTests(Chai): - def setUp(self): - super(LocaleTests, self).setUp() - - self.locale = locales.EnglishLocale() +class TestLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.EnglishLocale() def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 hours") - self.assertEqual(self.locale._format_timeframe("hour", 0), "an hour") + 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) - self.assertEqual(result, "just now") + assert result == "just now" def test_format_relative_past(self): result = self.locale._format_relative("an hour", "hour", 1) - self.assertEqual(result, "in an hour") + assert result == "in an hour" def test_format_relative_future(self): result = self.locale._format_relative("an hour", "hour", -1) - self.assertEqual(result, "an hour ago") + assert result == "an hour ago" def test_ordinal_number(self): - self.assertEqual(self.locale.ordinal_number(0), "0th") - self.assertEqual(self.locale.ordinal_number(1), "1st") - self.assertEqual(self.locale.ordinal_number(2), "2nd") - self.assertEqual(self.locale.ordinal_number(3), "3rd") - self.assertEqual(self.locale.ordinal_number(4), "4th") - self.assertEqual(self.locale.ordinal_number(10), "10th") - self.assertEqual(self.locale.ordinal_number(11), "11th") - self.assertEqual(self.locale.ordinal_number(12), "12th") - self.assertEqual(self.locale.ordinal_number(13), "13th") - self.assertEqual(self.locale.ordinal_number(14), "14th") - self.assertEqual(self.locale.ordinal_number(21), "21st") - self.assertEqual(self.locale.ordinal_number(22), "22nd") - self.assertEqual(self.locale.ordinal_number(23), "23rd") - self.assertEqual(self.locale.ordinal_number(24), "24th") - - self.assertEqual(self.locale.ordinal_number(100), "100th") - self.assertEqual(self.locale.ordinal_number(101), "101st") - self.assertEqual(self.locale.ordinal_number(102), "102nd") - self.assertEqual(self.locale.ordinal_number(103), "103rd") - self.assertEqual(self.locale.ordinal_number(104), "104th") - self.assertEqual(self.locale.ordinal_number(110), "110th") - self.assertEqual(self.locale.ordinal_number(111), "111th") - self.assertEqual(self.locale.ordinal_number(112), "112th") - self.assertEqual(self.locale.ordinal_number(113), "113th") - self.assertEqual(self.locale.ordinal_number(114), "114th") - self.assertEqual(self.locale.ordinal_number(121), "121st") - self.assertEqual(self.locale.ordinal_number(122), "122nd") - self.assertEqual(self.locale.ordinal_number(123), "123rd") - self.assertEqual(self.locale.ordinal_number(124), "124th") + assert self.locale.ordinal_number(0) == "0th" + assert self.locale.ordinal_number(1) == "1st" + assert self.locale.ordinal_number(2) == "2nd" + assert self.locale.ordinal_number(3) == "3rd" + assert self.locale.ordinal_number(4) == "4th" + assert self.locale.ordinal_number(10) == "10th" + assert self.locale.ordinal_number(11) == "11th" + assert self.locale.ordinal_number(12) == "12th" + assert self.locale.ordinal_number(13) == "13th" + assert self.locale.ordinal_number(14) == "14th" + assert self.locale.ordinal_number(21) == "21st" + assert self.locale.ordinal_number(22) == "22nd" + assert self.locale.ordinal_number(23) == "23rd" + assert self.locale.ordinal_number(24) == "24th" + + assert self.locale.ordinal_number(100) == "100th" + assert self.locale.ordinal_number(101) == "101st" + assert self.locale.ordinal_number(102) == "102nd" + assert self.locale.ordinal_number(103) == "103rd" + assert self.locale.ordinal_number(104) == "104th" + assert self.locale.ordinal_number(110) == "110th" + assert self.locale.ordinal_number(111) == "111th" + assert self.locale.ordinal_number(112) == "112th" + assert self.locale.ordinal_number(113) == "113th" + assert self.locale.ordinal_number(114) == "114th" + assert self.locale.ordinal_number(121) == "121st" + assert self.locale.ordinal_number(122) == "122nd" + assert self.locale.ordinal_number(123) == "123rd" + assert self.locale.ordinal_number(124) == "124th" def test_meridian_invalid_token(self): - self.assertEqual(self.locale.meridian(7, None), None) - self.assertEqual(self.locale.meridian(7, "B"), None) - self.assertEqual(self.locale.meridian(7, "NONSENSE"), None) - + assert self.locale.meridian(7, None) is None + assert self.locale.meridian(7, "B") is None + assert self.locale.meridian(7, "NONSENSE") is None -class EnglishLocaleTests(Chai): - def setUp(self): - super(EnglishLocaleTests, self).setUp() - self.locale = locales.EnglishLocale() +class TestEnglishLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.EnglishLocale() def test_describe(self): - self.assertEqual(self.locale.describe("now", only_distance=True), "instantly") - self.assertEqual(self.locale.describe("now", only_distance=False), "just now") + assert self.locale.describe("now", only_distance=True) == "instantly" + assert self.locale.describe("now", only_distance=False) == "just now" -class ItalianLocalesTests(Chai): +class TestItalianLocales: def test_ordinal_number(self): locale = locales.ItalianLocale() - self.assertEqual(locale.ordinal_number(1), "1º") + assert locale.ordinal_number(1) == "1º" -class SpanishLocalesTests(Chai): +class TestSpanishLocales: def test_ordinal_number(self): locale = locales.SpanishLocale() - self.assertEqual(locale.ordinal_number(1), "1º") + assert locale.ordinal_number(1) == "1º" def test_format_timeframe(self): locale = locales.SpanishLocale() - self.assertEqual(locale._format_timeframe("now", 0), "ahora") - self.assertEqual(locale._format_timeframe("seconds", 1), "1 segundos") - self.assertEqual(locale._format_timeframe("seconds", 3), "3 segundos") - self.assertEqual(locale._format_timeframe("seconds", 30), "30 segundos") - self.assertEqual(locale._format_timeframe("minute", 1), "un minuto") - self.assertEqual(locale._format_timeframe("minutes", 4), "4 minutos") - self.assertEqual(locale._format_timeframe("minutes", 40), "40 minutos") - self.assertEqual(locale._format_timeframe("hour", 1), "una hora") - self.assertEqual(locale._format_timeframe("hours", 5), "5 horas") - self.assertEqual(locale._format_timeframe("hours", 23), "23 horas") - self.assertEqual(locale._format_timeframe("day", 1), "un día") - self.assertEqual(locale._format_timeframe("days", 6), "6 días") - self.assertEqual(locale._format_timeframe("days", 12), "12 días") - self.assertEqual(locale._format_timeframe("week", 1), "una semana") - self.assertEqual(locale._format_timeframe("weeks", 2), "2 semanas") - self.assertEqual(locale._format_timeframe("weeks", 3), "3 semanas") - self.assertEqual(locale._format_timeframe("month", 1), "un mes") - self.assertEqual(locale._format_timeframe("months", 7), "7 meses") - self.assertEqual(locale._format_timeframe("months", 11), "11 meses") - self.assertEqual(locale._format_timeframe("year", 1), "un año") - self.assertEqual(locale._format_timeframe("years", 8), "8 años") - self.assertEqual(locale._format_timeframe("years", 12), "12 años") - - self.assertEqual(locale._format_timeframe("now", 0), "ahora") - self.assertEqual(locale._format_timeframe("seconds", -1), "1 segundos") - self.assertEqual(locale._format_timeframe("seconds", -9), "9 segundos") - self.assertEqual(locale._format_timeframe("seconds", -12), "12 segundos") - self.assertEqual(locale._format_timeframe("minute", -1), "un minuto") - self.assertEqual(locale._format_timeframe("minutes", -2), "2 minutos") - self.assertEqual(locale._format_timeframe("minutes", -10), "10 minutos") - self.assertEqual(locale._format_timeframe("hour", -1), "una hora") - self.assertEqual(locale._format_timeframe("hours", -3), "3 horas") - self.assertEqual(locale._format_timeframe("hours", -11), "11 horas") - self.assertEqual(locale._format_timeframe("day", -1), "un día") - self.assertEqual(locale._format_timeframe("days", -2), "2 días") - self.assertEqual(locale._format_timeframe("days", -12), "12 días") - self.assertEqual(locale._format_timeframe("week", -1), "una semana") - self.assertEqual(locale._format_timeframe("weeks", -2), "2 semanas") - self.assertEqual(locale._format_timeframe("weeks", -3), "3 semanas") - self.assertEqual(locale._format_timeframe("month", -1), "un mes") - self.assertEqual(locale._format_timeframe("months", -3), "3 meses") - self.assertEqual(locale._format_timeframe("months", -13), "13 meses") - self.assertEqual(locale._format_timeframe("year", -1), "un año") - self.assertEqual(locale._format_timeframe("years", -4), "4 años") - self.assertEqual(locale._format_timeframe("years", -14), "14 años") - - -class FrenchLocalesTests(Chai): + assert locale._format_timeframe("now", 0) == "ahora" + assert locale._format_timeframe("seconds", 1) == "1 segundos" + assert locale._format_timeframe("seconds", 3) == "3 segundos" + assert locale._format_timeframe("seconds", 30) == "30 segundos" + assert locale._format_timeframe("minute", 1) == "un minuto" + assert locale._format_timeframe("minutes", 4) == "4 minutos" + assert locale._format_timeframe("minutes", 40) == "40 minutos" + assert locale._format_timeframe("hour", 1) == "una hora" + assert locale._format_timeframe("hours", 5) == "5 horas" + assert locale._format_timeframe("hours", 23) == "23 horas" + assert locale._format_timeframe("day", 1) == "un día" + assert locale._format_timeframe("days", 6) == "6 días" + assert locale._format_timeframe("days", 12) == "12 días" + assert locale._format_timeframe("week", 1) == "una semana" + assert locale._format_timeframe("weeks", 2) == "2 semanas" + assert locale._format_timeframe("weeks", 3) == "3 semanas" + assert locale._format_timeframe("month", 1) == "un mes" + assert locale._format_timeframe("months", 7) == "7 meses" + assert locale._format_timeframe("months", 11) == "11 meses" + assert locale._format_timeframe("year", 1) == "un año" + assert locale._format_timeframe("years", 8) == "8 años" + assert locale._format_timeframe("years", 12) == "12 años" + + assert locale._format_timeframe("now", 0) == "ahora" + assert locale._format_timeframe("seconds", -1) == "1 segundos" + assert locale._format_timeframe("seconds", -9) == "9 segundos" + assert locale._format_timeframe("seconds", -12) == "12 segundos" + assert locale._format_timeframe("minute", -1) == "un minuto" + assert locale._format_timeframe("minutes", -2) == "2 minutos" + assert locale._format_timeframe("minutes", -10) == "10 minutos" + assert locale._format_timeframe("hour", -1) == "una hora" + assert locale._format_timeframe("hours", -3) == "3 horas" + assert locale._format_timeframe("hours", -11) == "11 horas" + assert locale._format_timeframe("day", -1) == "un día" + assert locale._format_timeframe("days", -2) == "2 días" + assert locale._format_timeframe("days", -12) == "12 días" + assert locale._format_timeframe("week", -1) == "una semana" + assert locale._format_timeframe("weeks", -2) == "2 semanas" + assert locale._format_timeframe("weeks", -3) == "3 semanas" + assert locale._format_timeframe("month", -1) == "un mes" + assert locale._format_timeframe("months", -3) == "3 meses" + assert locale._format_timeframe("months", -13) == "13 meses" + assert locale._format_timeframe("year", -1) == "un año" + assert locale._format_timeframe("years", -4) == "4 años" + assert locale._format_timeframe("years", -14) == "14 años" + + +class TestFrenchLocales: def test_ordinal_number(self): locale = locales.FrenchLocale() - self.assertEqual(locale.ordinal_number(1), "1er") - self.assertEqual(locale.ordinal_number(2), "2e") + assert locale.ordinal_number(1) == "1er" + assert locale.ordinal_number(2) == "2e" -class RussianLocalesTests(Chai): +class TestRussianLocales: def test_plurals2(self): locale = locales.RussianLocale() - self.assertEqual(locale._format_timeframe("hours", 0), "0 часов") - self.assertEqual(locale._format_timeframe("hours", 1), "1 час") - self.assertEqual(locale._format_timeframe("hours", 2), "2 часа") - self.assertEqual(locale._format_timeframe("hours", 4), "4 часа") - self.assertEqual(locale._format_timeframe("hours", 5), "5 часов") - self.assertEqual(locale._format_timeframe("hours", 21), "21 час") - self.assertEqual(locale._format_timeframe("hours", 22), "22 часа") - self.assertEqual(locale._format_timeframe("hours", 25), "25 часов") + assert locale._format_timeframe("hours", 0) == "0 часов" + assert locale._format_timeframe("hours", 1) == "1 час" + assert locale._format_timeframe("hours", 2) == "2 часа" + assert locale._format_timeframe("hours", 4) == "4 часа" + assert locale._format_timeframe("hours", 5) == "5 часов" + assert locale._format_timeframe("hours", 21) == "21 час" + assert locale._format_timeframe("hours", 22) == "22 часа" + assert locale._format_timeframe("hours", 25) == "25 часов" # feminine grammatical gender should be tested separately - self.assertEqual(locale._format_timeframe("minutes", 0), "0 минут") - self.assertEqual(locale._format_timeframe("minutes", 1), "1 минуту") - self.assertEqual(locale._format_timeframe("minutes", 2), "2 минуты") - self.assertEqual(locale._format_timeframe("minutes", 4), "4 минуты") - self.assertEqual(locale._format_timeframe("minutes", 5), "5 минут") - self.assertEqual(locale._format_timeframe("minutes", 21), "21 минуту") - self.assertEqual(locale._format_timeframe("minutes", 22), "22 минуты") - self.assertEqual(locale._format_timeframe("minutes", 25), "25 минут") + assert locale._format_timeframe("minutes", 0) == "0 минут" + assert locale._format_timeframe("minutes", 1) == "1 минуту" + assert locale._format_timeframe("minutes", 2) == "2 минуты" + assert locale._format_timeframe("minutes", 4) == "4 минуты" + assert locale._format_timeframe("minutes", 5) == "5 минут" + assert locale._format_timeframe("minutes", 21) == "21 минуту" + assert locale._format_timeframe("minutes", 22) == "22 минуты" + assert locale._format_timeframe("minutes", 25) == "25 минут" -class PolishLocalesTests(Chai): +class TestPolishLocales: def test_plurals(self): locale = locales.PolishLocale() - self.assertEqual(locale._format_timeframe("hours", 0), "0 godzin") - self.assertEqual(locale._format_timeframe("hours", 1), "1 godzin") - self.assertEqual(locale._format_timeframe("hours", 2), "2 godziny") - self.assertEqual(locale._format_timeframe("hours", 4), "4 godziny") - self.assertEqual(locale._format_timeframe("hours", 5), "5 godzin") - self.assertEqual(locale._format_timeframe("hours", 21), "21 godzin") - self.assertEqual(locale._format_timeframe("hours", 22), "22 godziny") - self.assertEqual(locale._format_timeframe("hours", 25), "25 godzin") + assert locale._format_timeframe("hours", 0) == "0 godzin" + assert locale._format_timeframe("hours", 1) == "1 godzin" + assert locale._format_timeframe("hours", 2) == "2 godziny" + assert locale._format_timeframe("hours", 4) == "4 godziny" + assert locale._format_timeframe("hours", 5) == "5 godzin" + assert locale._format_timeframe("hours", 21) == "21 godzin" + assert locale._format_timeframe("hours", 22) == "22 godziny" + assert locale._format_timeframe("hours", 25) == "25 godzin" -class IcelandicLocalesTests(Chai): - def setUp(self): - super(IcelandicLocalesTests, self).setUp() - - self.locale = locales.IcelandicLocale() +class TestIcelandicLocales: + @classmethod + def setup_class(cls): + cls.locale = locales.IcelandicLocale() def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("minute", -1), "einni mínútu") - self.assertEqual(self.locale._format_timeframe("minute", 1), "eina mínútu") - - self.assertEqual(self.locale._format_timeframe("hours", -2), "2 tímum") - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 tíma") - self.assertEqual(self.locale._format_timeframe("now", 0), "rétt í þessu") + assert self.locale._format_timeframe("minute", -1) == "einni mínútu" + assert self.locale._format_timeframe("minute", 1) == "eina mínútu" + assert self.locale._format_timeframe("hours", -2) == "2 tímum" + assert self.locale._format_timeframe("hours", 2) == "2 tíma" + assert self.locale._format_timeframe("now", 0) == "rétt í þessu" -class MalayalamLocaleTests(Chai): - def setUp(self): - super(MalayalamLocaleTests, self).setUp() - self.locale = locales.MalayalamLocale() +class TestMalayalamLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.MalayalamLocale() def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 മണിക്കൂർ") - self.assertEqual(self.locale._format_timeframe("hour", 0), "ഒരു മണിക്കൂർ") + 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) - self.assertEqual(result, "ഇപ്പോൾ") + assert result == "ഇപ്പോൾ" def test_format_relative_past(self): result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", 1) - self.assertEqual(result, "ഒരു മണിക്കൂർ ശേഷം") + assert result == "ഒരു മണിക്കൂർ ശേഷം" def test_format_relative_future(self): result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", -1) - self.assertEqual(result, "ഒരു മണിക്കൂർ മുമ്പ്") - + assert result == "ഒരു മണിക്കൂർ മുമ്പ്" -class HindiLocaleTests(Chai): - def setUp(self): - super(HindiLocaleTests, self).setUp() - self.locale = locales.HindiLocale() +class TestHindiLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.HindiLocale() def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 घंटे") - self.assertEqual(self.locale._format_timeframe("hour", 0), "एक घंटा") + 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) - self.assertEqual(result, "अभी") + assert result == "अभी" def test_format_relative_past(self): result = self.locale._format_relative("एक घंटा", "hour", 1) - self.assertEqual(result, "एक घंटा बाद") + assert result == "एक घंटा बाद" def test_format_relative_future(self): result = self.locale._format_relative("एक घंटा", "hour", -1) - self.assertEqual(result, "एक घंटा पहले") + assert result == "एक घंटा पहले" -class CzechLocaleTests(Chai): - def setUp(self): - super(CzechLocaleTests, self).setUp() - - self.locale = locales.CzechLocale() +class TestCzechLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.CzechLocale() def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 hodiny") - self.assertEqual(self.locale._format_timeframe("hours", 5), "5 hodin") - self.assertEqual(self.locale._format_timeframe("hour", 0), "0 hodin") - self.assertEqual(self.locale._format_timeframe("hours", -2), "2 hodinami") - self.assertEqual(self.locale._format_timeframe("hours", -5), "5 hodinami") - self.assertEqual(self.locale._format_timeframe("now", 0), "Teď") + assert self.locale._format_timeframe("hours", 2) == "2 hodiny" + assert self.locale._format_timeframe("hours", 5) == "5 hodin" + assert self.locale._format_timeframe("hour", 0) == "0 hodin" + assert self.locale._format_timeframe("hours", -2) == "2 hodinami" + assert self.locale._format_timeframe("hours", -5) == "5 hodinami" + assert self.locale._format_timeframe("now", 0) == "Teď" def test_format_relative_now(self): result = self.locale._format_relative("Teď", "now", 0) - self.assertEqual(result, "Teď") + assert result == "Teď" def test_format_relative_future(self): result = self.locale._format_relative("hodinu", "hour", 1) - self.assertEqual(result, "Za hodinu") + assert result == "Za hodinu" def test_format_relative_past(self): result = self.locale._format_relative("hodinou", "hour", -1) - self.assertEqual(result, "Před hodinou") - + assert result == "Před hodinou" -class SlovakLocaleTests(Chai): - def setUp(self): - super(SlovakLocaleTests, self).setUp() - self.locale = locales.SlovakLocale() +class TestSlovakLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.SlovakLocale() def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 hodiny") - self.assertEqual(self.locale._format_timeframe("hours", 5), "5 hodín") - self.assertEqual(self.locale._format_timeframe("hour", 0), "0 hodín") - self.assertEqual(self.locale._format_timeframe("hours", -2), "2 hodinami") - self.assertEqual(self.locale._format_timeframe("hours", -5), "5 hodinami") - self.assertEqual(self.locale._format_timeframe("now", 0), "Teraz") + assert self.locale._format_timeframe("hours", 2) == "2 hodiny" + assert self.locale._format_timeframe("hours", 5) == "5 hodín" + assert self.locale._format_timeframe("hour", 0) == "0 hodín" + assert self.locale._format_timeframe("hours", -2) == "2 hodinami" + assert self.locale._format_timeframe("hours", -5) == "5 hodinami" + assert self.locale._format_timeframe("now", 0) == "Teraz" def test_format_relative_now(self): result = self.locale._format_relative("Teraz", "now", 0) - self.assertEqual(result, "Teraz") + assert result == "Teraz" def test_format_relative_future(self): result = self.locale._format_relative("hodinu", "hour", 1) - self.assertEqual(result, "O hodinu") + assert result == "O hodinu" def test_format_relative_past(self): result = self.locale._format_relative("hodinou", "hour", -1) - self.assertEqual(result, "Pred hodinou") + assert result == "Pred hodinou" -class BulgarianLocaleTests(Chai): +class TestBulgarianLocale: def test_plurals2(self): locale = locales.BulgarianLocale() - self.assertEqual(locale._format_timeframe("hours", 0), "0 часа") - self.assertEqual(locale._format_timeframe("hours", 1), "1 час") - self.assertEqual(locale._format_timeframe("hours", 2), "2 часа") - self.assertEqual(locale._format_timeframe("hours", 4), "4 часа") - self.assertEqual(locale._format_timeframe("hours", 5), "5 часа") - self.assertEqual(locale._format_timeframe("hours", 21), "21 час") - self.assertEqual(locale._format_timeframe("hours", 22), "22 часа") - self.assertEqual(locale._format_timeframe("hours", 25), "25 часа") + assert locale._format_timeframe("hours", 0) == "0 часа" + assert locale._format_timeframe("hours", 1) == "1 час" + assert locale._format_timeframe("hours", 2) == "2 часа" + assert locale._format_timeframe("hours", 4) == "4 часа" + assert locale._format_timeframe("hours", 5) == "5 часа" + assert locale._format_timeframe("hours", 21) == "21 час" + assert locale._format_timeframe("hours", 22) == "22 часа" + assert locale._format_timeframe("hours", 25) == "25 часа" # feminine grammatical gender should be tested separately - self.assertEqual(locale._format_timeframe("minutes", 0), "0 минути") - self.assertEqual(locale._format_timeframe("minutes", 1), "1 минута") - self.assertEqual(locale._format_timeframe("minutes", 2), "2 минути") - self.assertEqual(locale._format_timeframe("minutes", 4), "4 минути") - self.assertEqual(locale._format_timeframe("minutes", 5), "5 минути") - self.assertEqual(locale._format_timeframe("minutes", 21), "21 минута") - self.assertEqual(locale._format_timeframe("minutes", 22), "22 минути") - self.assertEqual(locale._format_timeframe("minutes", 25), "25 минути") + assert locale._format_timeframe("minutes", 0) == "0 минути" + assert locale._format_timeframe("minutes", 1) == "1 минута" + assert locale._format_timeframe("minutes", 2) == "2 минути" + assert locale._format_timeframe("minutes", 4) == "4 минути" + assert locale._format_timeframe("minutes", 5) == "5 минути" + assert locale._format_timeframe("minutes", 21) == "21 минута" + assert locale._format_timeframe("minutes", 22) == "22 минути" + assert locale._format_timeframe("minutes", 25) == "25 минути" -class MacedonianLocaleTests(Chai): +class TestMacedonianLocale: def test_plurals_mk(self): locale = locales.MacedonianLocale() # time - self.assertEqual(locale._format_relative("сега", "now", 0), "сега") + assert locale._format_relative("сега", "now", 0) == "сега" # Hours - self.assertEqual(locale._format_timeframe("hours", 0), "0 саати") - self.assertEqual(locale._format_timeframe("hours", 1), "1 саат") - self.assertEqual(locale._format_timeframe("hours", 2), "2 саати") - self.assertEqual(locale._format_timeframe("hours", 4), "4 саати") - self.assertEqual(locale._format_timeframe("hours", 5), "5 саати") - self.assertEqual(locale._format_timeframe("hours", 21), "21 саат") - self.assertEqual(locale._format_timeframe("hours", 22), "22 саати") - self.assertEqual(locale._format_timeframe("hours", 25), "25 саати") + assert locale._format_timeframe("hours", 0) == "0 саати" + assert locale._format_timeframe("hours", 1) == "1 саат" + assert locale._format_timeframe("hours", 2) == "2 саати" + assert locale._format_timeframe("hours", 4) == "4 саати" + assert locale._format_timeframe("hours", 5) == "5 саати" + assert locale._format_timeframe("hours", 21) == "21 саат" + assert locale._format_timeframe("hours", 22) == "22 саати" + assert locale._format_timeframe("hours", 25) == "25 саати" # Minutes - self.assertEqual(locale._format_timeframe("minutes", 0), "0 минути") - self.assertEqual(locale._format_timeframe("minutes", 1), "1 минута") - self.assertEqual(locale._format_timeframe("minutes", 2), "2 минути") - self.assertEqual(locale._format_timeframe("minutes", 4), "4 минути") - self.assertEqual(locale._format_timeframe("minutes", 5), "5 минути") - self.assertEqual(locale._format_timeframe("minutes", 21), "21 минута") - self.assertEqual(locale._format_timeframe("minutes", 22), "22 минути") - self.assertEqual(locale._format_timeframe("minutes", 25), "25 минути") + assert locale._format_timeframe("minutes", 0) == "0 минути" + assert locale._format_timeframe("minutes", 1) == "1 минута" + assert locale._format_timeframe("minutes", 2) == "2 минути" + assert locale._format_timeframe("minutes", 4) == "4 минути" + assert locale._format_timeframe("minutes", 5) == "5 минути" + assert locale._format_timeframe("minutes", 21) == "21 минута" + assert locale._format_timeframe("minutes", 22) == "22 минути" + assert locale._format_timeframe("minutes", 25) == "25 минути" -class HebrewLocaleTests(Chai): +class TestHebrewLocale: def test_couple_of_timeframe(self): locale = locales.HebrewLocale() - self.assertEqual(locale._format_timeframe("hours", 2), "שעתיים") - self.assertEqual(locale._format_timeframe("months", 2), "חודשיים") - self.assertEqual(locale._format_timeframe("days", 2), "יומיים") - self.assertEqual(locale._format_timeframe("years", 2), "שנתיים") - - self.assertEqual(locale._format_timeframe("hours", 3), "3 שעות") - self.assertEqual(locale._format_timeframe("months", 4), "4 חודשים") - self.assertEqual(locale._format_timeframe("days", 3), "3 ימים") - self.assertEqual(locale._format_timeframe("years", 5), "5 שנים") + assert locale._format_timeframe("hours", 2) == "שעתיים" + assert locale._format_timeframe("months", 2) == "חודשיים" + assert locale._format_timeframe("days", 2) == "יומיים" + assert locale._format_timeframe("years", 2) == "שנתיים" + assert locale._format_timeframe("hours", 3) == "3 שעות" + assert locale._format_timeframe("months", 4) == "4 חודשים" + assert locale._format_timeframe("days", 3) == "3 ימים" + assert locale._format_timeframe("years", 5) == "5 שנים" -class MarathiLocaleTests(Chai): - def setUp(self): - super(MarathiLocaleTests, self).setUp() - self.locale = locales.MarathiLocale() +class TestMarathiLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.MarathiLocale() def test_dateCoreFunctionality(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) - self.assertEqual(self.locale.month_name(dt.month), "एप्रिल") - self.assertEqual(self.locale.month_abbreviation(dt.month), "एप्रि") - self.assertEqual(self.locale.day_name(dt.isoweekday()), "शनिवार") - self.assertEqual(self.locale.day_abbreviation(dt.isoweekday()), "शनि") + assert self.locale.month_name(dt.month) == "एप्रिल" + assert self.locale.month_abbreviation(dt.month) == "एप्रि" + assert self.locale.day_name(dt.isoweekday()) == "शनिवार" + assert self.locale.day_abbreviation(dt.isoweekday()) == "शनि" def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 तास") - self.assertEqual(self.locale._format_timeframe("hour", 0), "एक तास") + 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) - self.assertEqual(result, "सद्य") + assert result == "सद्य" def test_format_relative_past(self): result = self.locale._format_relative("एक तास", "hour", 1) - self.assertEqual(result, "एक तास नंतर") + assert result == "एक तास नंतर" def test_format_relative_future(self): result = self.locale._format_relative("एक तास", "hour", -1) - self.assertEqual(result, "एक तास आधी") + assert result == "एक तास आधी" # Not currently implemented def test_ordinal_number(self): - self.assertEqual(self.locale.ordinal_number(1), "1") + assert self.locale.ordinal_number(1) == "1" -class FinnishLocaleTests(Chai): - def setUp(self): - super(FinnishLocaleTests, self).setUp() - - self.locale = locales.FinnishLocale() +class TestFinnishLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.FinnishLocale() def test_format_timeframe(self): - self.assertEqual( - self.locale._format_timeframe("hours", 2), ("2 tuntia", "2 tunnin") - ) - self.assertEqual(self.locale._format_timeframe("hour", 0), ("tunti", "tunnin")) + assert self.locale._format_timeframe("hours", 2) == ("2 tuntia", "2 tunnin") + assert self.locale._format_timeframe("hour", 0) == ("tunti", "tunnin") def test_format_relative_now(self): result = self.locale._format_relative(["juuri nyt", "juuri nyt"], "now", 0) - self.assertEqual(result, "juuri nyt") + assert result == "juuri nyt" def test_format_relative_past(self): result = self.locale._format_relative(["tunti", "tunnin"], "hour", 1) - self.assertEqual(result, "tunnin kuluttua") + assert result == "tunnin kuluttua" def test_format_relative_future(self): result = self.locale._format_relative(["tunti", "tunnin"], "hour", -1) - self.assertEqual(result, "tunti sitten") + assert result == "tunti sitten" def test_ordinal_number(self): - self.assertEqual(self.locale.ordinal_number(1), "1.") - + assert self.locale.ordinal_number(1) == "1." -class GermanLocaleTests(Chai): - def setUp(self): - super(GermanLocaleTests, self).setUp() - self.locale = locales.GermanLocale() +class TestGermanLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.GermanLocale() def test_ordinal_number(self): - self.assertEqual(self.locale.ordinal_number(1), "1.") + assert self.locale.ordinal_number(1) == "1." def test_define(self): - self.assertEqual( - self.locale.describe("minute", only_distance=True), "eine Minute" - ) - self.assertEqual( - self.locale.describe("minute", only_distance=False), "in einer Minute" - ) - self.assertEqual( - self.locale.describe("hour", only_distance=True), "eine Stunde" - ) - self.assertEqual( - self.locale.describe("hour", only_distance=False), "in einer Stunde" - ) - self.assertEqual(self.locale.describe("day", only_distance=True), "ein Tag") - self.assertEqual( - self.locale.describe("day", only_distance=False), "in einem Tag" - ) - self.assertEqual(self.locale.describe("month", only_distance=True), "ein Monat") - self.assertEqual( - self.locale.describe("month", only_distance=False), "in einem Monat" - ) - self.assertEqual(self.locale.describe("year", only_distance=True), "ein Jahr") - self.assertEqual( - self.locale.describe("year", only_distance=False), "in einem Jahr" - ) - - -class HungarianLocaleTests(Chai): - def setUp(self): - super(HungarianLocaleTests, self).setUp() - - self.locale = locales.HungarianLocale() + assert self.locale.describe("minute", only_distance=True) == "eine Minute" + assert self.locale.describe("minute", only_distance=False) == "in einer Minute" + assert self.locale.describe("hour", only_distance=True) == "eine Stunde" + assert self.locale.describe("hour", only_distance=False) == "in einer Stunde" + assert self.locale.describe("day", only_distance=True) == "ein Tag" + assert self.locale.describe("day", only_distance=False) == "in einem Tag" + assert self.locale.describe("month", only_distance=True) == "ein Monat" + assert self.locale.describe("month", only_distance=False) == "in einem Monat" + assert self.locale.describe("year", only_distance=True) == "ein Jahr" + assert self.locale.describe("year", only_distance=False) == "in einem Jahr" + + +class TestHungarianLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.HungarianLocale() def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 óra") - self.assertEqual(self.locale._format_timeframe("hour", 0), "egy órával") - self.assertEqual(self.locale._format_timeframe("hours", -2), "2 órával") - self.assertEqual(self.locale._format_timeframe("now", 0), "éppen most") - + assert self.locale._format_timeframe("hours", 2) == "2 óra" + assert self.locale._format_timeframe("hour", 0) == "egy órával" + assert self.locale._format_timeframe("hours", -2) == "2 órával" + assert self.locale._format_timeframe("now", 0) == "éppen most" -class EsperantoLocaleTests(Chai): - def setUp(self): - super(EsperantoLocaleTests, self).setUp() - self.locale = locales.EsperantoLocale() +class TestEsperantoLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.EsperantoLocale() def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 horoj") - self.assertEqual(self.locale._format_timeframe("hour", 0), "un horo") - self.assertEqual(self.locale._format_timeframe("hours", -2), "2 horoj") - self.assertEqual(self.locale._format_timeframe("now", 0), "nun") + assert self.locale._format_timeframe("hours", 2) == "2 horoj" + assert self.locale._format_timeframe("hour", 0) == "un horo" + assert self.locale._format_timeframe("hours", -2) == "2 horoj" + assert self.locale._format_timeframe("now", 0) == "nun" def test_ordinal_number(self): - self.assertEqual(self.locale.ordinal_number(1), "1a") + assert self.locale.ordinal_number(1) == "1a" -class ThaiLocaleTests(Chai): - def setUp(self): - super(ThaiLocaleTests, self).setUp() - - self.locale = locales.ThaiLocale() +class TestThaiLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.ThaiLocale() def test_year_full(self): - self.assertEqual(self.locale.year_full(2015), "2558") + assert self.locale.year_full(2015) == "2558" def test_year_abbreviation(self): - self.assertEqual(self.locale.year_abbreviation(2015), "58") + assert self.locale.year_abbreviation(2015) == "58" def test_format_relative_now(self): result = self.locale._format_relative("ขณะนี้", "now", 0) - self.assertEqual(result, "ขณะนี้") + assert result == "ขณะนี้" def test_format_relative_past(self): result = self.locale._format_relative("1 ชั่วโมง", "hour", 1) - self.assertEqual(result, "ในอีก 1 ชั่วโมง") + assert result == "ในอีก 1 ชั่วโมง" result = self.locale._format_relative("{0} ชั่วโมง", "hours", 2) - self.assertEqual(result, "ในอีก {0} ชั่วโมง") + assert result == "ในอีก {0} ชั่วโมง" result = self.locale._format_relative("ไม่กี่วินาที", "seconds", 42) - self.assertEqual(result, "ในอีกไม่กี่วินาที") + assert result == "ในอีกไม่กี่วินาที" def test_format_relative_future(self): result = self.locale._format_relative("1 ชั่วโมง", "hour", -1) - self.assertEqual(result, "1 ชั่วโมง ที่ผ่านมา") - + assert result == "1 ชั่วโมง ที่ผ่านมา" -class BengaliLocaleTests(Chai): - def setUp(self): - super(BengaliLocaleTests, self).setUp() - self.locale = locales.BengaliLocale() +class TestBengaliLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.BengaliLocale() def test_ordinal_number(self): result0 = self.locale._ordinal_number(0) @@ -622,333 +592,295 @@ def test_ordinal_number(self): result10 = self.locale._ordinal_number(10) result11 = self.locale._ordinal_number(11) result42 = self.locale._ordinal_number(42) - self.assertEqual(result0, "0তম") - self.assertEqual(result1, "1ম") - self.assertEqual(result3, "3য়") - self.assertEqual(result4, "4র্থ") - self.assertEqual(result5, "5ম") - self.assertEqual(result6, "6ষ্ঠ") - self.assertEqual(result10, "10ম") - self.assertEqual(result11, "11তম") - self.assertEqual(result42, "42তম") - self.assertEqual(self.locale._ordinal_number(-1), None) - - -class SwissLocaleTests(Chai): - def setUp(self): - super(SwissLocaleTests, self).setUp() - - self.locale = locales.SwissLocale() + assert result0 == "0তম" + assert result1 == "1ম" + assert result3 == "3য়" + assert result4 == "4র্থ" + assert result5 == "5ম" + assert result6 == "6ষ্ঠ" + assert result10 == "10ম" + assert result11 == "11তম" + assert result42 == "42তম" + assert self.locale._ordinal_number(-1) is None + + +class TestSwissLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.SwissLocale() def test_ordinal_number(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) - self.assertEqual(self.locale._format_timeframe("minute", 1), "einer Minute") - self.assertEqual(self.locale._format_timeframe("hour", 1), "einer Stunde") - self.assertEqual(self.locale.day_abbreviation(dt.isoweekday()), "Sa") - + assert self.locale._format_timeframe("minute", 1) == "einer Minute" + assert self.locale._format_timeframe("hour", 1) == "einer Stunde" + assert self.locale.day_abbreviation(dt.isoweekday()) == "Sa" -class RomanianLocaleTests(Chai): - def setUp(self): - super(RomanianLocaleTests, self).setUp() - self.locale = locales.RomanianLocale() +class TestRomanianLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.RomanianLocale() def test_timeframes(self): - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 ore") - self.assertEqual(self.locale._format_timeframe("months", 2), "2 luni") + assert self.locale._format_timeframe("hours", 2) == "2 ore" + assert self.locale._format_timeframe("months", 2) == "2 luni" - self.assertEqual(self.locale._format_timeframe("days", 2), "2 zile") - self.assertEqual(self.locale._format_timeframe("years", 2), "2 ani") + assert self.locale._format_timeframe("days", 2) == "2 zile" + assert self.locale._format_timeframe("years", 2) == "2 ani" - self.assertEqual(self.locale._format_timeframe("hours", 3), "3 ore") - self.assertEqual(self.locale._format_timeframe("months", 4), "4 luni") - self.assertEqual(self.locale._format_timeframe("days", 3), "3 zile") - self.assertEqual(self.locale._format_timeframe("years", 5), "5 ani") + assert self.locale._format_timeframe("hours", 3) == "3 ore" + assert self.locale._format_timeframe("months", 4) == "4 luni" + assert self.locale._format_timeframe("days", 3) == "3 zile" + assert self.locale._format_timeframe("years", 5) == "5 ani" def test_relative_timeframes(self): - self.assertEqual(self.locale._format_relative("acum", "now", 0), "acum") - self.assertEqual( - self.locale._format_relative("o oră", "hour", 1), "peste o oră" - ) - self.assertEqual( - self.locale._format_relative("o oră", "hour", -1), "o oră în urmă" - ) - self.assertEqual( - self.locale._format_relative("un minut", "minute", 1), "peste un minut" - ) - self.assertEqual( - self.locale._format_relative("un minut", "minute", -1), "un minut în urmă" - ) - self.assertEqual( - self.locale._format_relative("câteva secunde", "seconds", -1), - "câteva secunde în urmă", - ) - self.assertEqual( - self.locale._format_relative("câteva secunde", "seconds", 1), - "peste câteva secunde", - ) - self.assertEqual( - self.locale._format_relative("o zi", "day", -1), "o zi în urmă" - ) - self.assertEqual(self.locale._format_relative("o zi", "day", 1), "peste o zi") - - -class ArabicLocalesTest(Chai): - def setUp(self): - super(ArabicLocalesTest, self).setUp() - - self.locale = locales.ArabicLocale() + assert self.locale._format_relative("acum", "now", 0) == "acum" + assert self.locale._format_relative("o oră", "hour", 1) == "peste o oră" + assert self.locale._format_relative("o oră", "hour", -1) == "o oră în urmă" + assert self.locale._format_relative("un minut", "minute", 1) == "peste un minut" + assert self.locale._format_relative("un minut", "minute", -1) == "un minut în urmă" + assert self.locale._format_relative("câteva secunde", "seconds", -1) == \ + "câteva secunde în urmă" + assert self.locale._format_relative("câteva secunde", "seconds", 1) == \ + "peste câteva secunde" + assert self.locale._format_relative("o zi", "day", -1) == "o zi în urmă" + assert self.locale._format_relative("o zi", "day", 1) == "peste o zi" + + +class TestArabicLocales: + @classmethod + def setup_class(cls): + cls.locale = locales.ArabicLocale() def test_timeframes(self): # single - self.assertEqual(self.locale._format_timeframe("minute", 1), "دقيقة") - self.assertEqual(self.locale._format_timeframe("hour", 1), "ساعة") - self.assertEqual(self.locale._format_timeframe("day", 1), "يوم") - self.assertEqual(self.locale._format_timeframe("month", 1), "شهر") - self.assertEqual(self.locale._format_timeframe("year", 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) == "سنة" # double - self.assertEqual(self.locale._format_timeframe("minutes", 2), "دقيقتين") - self.assertEqual(self.locale._format_timeframe("hours", 2), "ساعتين") - self.assertEqual(self.locale._format_timeframe("days", 2), "يومين") - self.assertEqual(self.locale._format_timeframe("months", 2), "شهرين") - self.assertEqual(self.locale._format_timeframe("years", 2), "سنتين") + 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("months", 2) == "شهرين" + assert self.locale._format_timeframe("years", 2) == "سنتين" # up to ten - self.assertEqual(self.locale._format_timeframe("minutes", 3), "3 دقائق") - self.assertEqual(self.locale._format_timeframe("hours", 4), "4 ساعات") - self.assertEqual(self.locale._format_timeframe("days", 5), "5 أيام") - self.assertEqual(self.locale._format_timeframe("months", 6), "6 أشهر") - self.assertEqual(self.locale._format_timeframe("years", 10), "10 سنوات") + 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("months", 6) == "6 أشهر" + assert self.locale._format_timeframe("years", 10) == "10 سنوات" # more than ten - self.assertEqual(self.locale._format_timeframe("minutes", 11), "11 دقيقة") - self.assertEqual(self.locale._format_timeframe("hours", 19), "19 ساعة") - self.assertEqual(self.locale._format_timeframe("months", 24), "24 شهر") - self.assertEqual(self.locale._format_timeframe("days", 50), "50 يوم") - self.assertEqual(self.locale._format_timeframe("years", 115), "115 سنة") + assert self.locale._format_timeframe("minutes", 11) == "11 دقيقة" + assert self.locale._format_timeframe("hours", 19) == "19 ساعة" + assert self.locale._format_timeframe("months", 24) == "24 شهر" + assert self.locale._format_timeframe("days", 50) == "50 يوم" + assert self.locale._format_timeframe("years", 115) == "115 سنة" -class NepaliLocaleTests(Chai): - def setUp(self): - super(NepaliLocaleTests, self).setUp() - - self.locale = locales.NepaliLocale() +class TestNepaliLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.NepaliLocale() def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("hours", 3), "3 घण्टा") - self.assertEqual(self.locale._format_timeframe("hour", 0), "एक घण्टा") + assert self.locale._format_timeframe("hours", 3) == "3 घण्टा" + assert self.locale._format_timeframe("hour", 0) == "एक घण्टा" def test_format_relative_now(self): result = self.locale._format_relative("अहिले", "now", 0) - self.assertEqual(result, "अहिले") + assert result == "अहिले" def test_format_relative_future(self): result = self.locale._format_relative("एक घण्टा", "hour", 1) - self.assertEqual(result, "एक घण्टा पछी") + assert result == "एक घण्टा पछी" def test_format_relative_past(self): result = self.locale._format_relative("एक घण्टा", "hour", -1) - self.assertEqual(result, "एक घण्टा पहिले") - + assert result == "एक घण्टा पहिले" -class IndonesianLocaleTests(Chai): - def setUp(self): - super(IndonesianLocaleTests, self).setUp() - self.locale = locales.IndonesianLocale() +class TestIndonesianLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.IndonesianLocale() def test_timeframes(self): - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 jam") - self.assertEqual(self.locale._format_timeframe("months", 2), "2 bulan") + assert self.locale._format_timeframe("hours", 2) == "2 jam" + assert self.locale._format_timeframe("months", 2) == "2 bulan" - self.assertEqual(self.locale._format_timeframe("days", 2), "2 hari") - self.assertEqual(self.locale._format_timeframe("years", 2), "2 tahun") + assert self.locale._format_timeframe("days", 2) == "2 hari" + assert self.locale._format_timeframe("years", 2) == "2 tahun" - self.assertEqual(self.locale._format_timeframe("hours", 3), "3 jam") - self.assertEqual(self.locale._format_timeframe("months", 4), "4 bulan") - self.assertEqual(self.locale._format_timeframe("days", 3), "3 hari") - self.assertEqual(self.locale._format_timeframe("years", 5), "5 tahun") + assert self.locale._format_timeframe("hours", 3) == "3 jam" + assert self.locale._format_timeframe("months", 4) == "4 bulan" + assert self.locale._format_timeframe("days", 3) == "3 hari" + assert self.locale._format_timeframe("years", 5) == "5 tahun" def test_format_relative_now(self): - self.assertEqual( - self.locale._format_relative("baru saja", "now", 0), "baru saja" - ) + assert self.locale._format_relative("baru saja", "now", 0) == "baru saja" def test_format_relative_past(self): - self.assertEqual( - self.locale._format_relative("1 jam", "hour", 1), "dalam 1 jam" - ) - self.assertEqual( - self.locale._format_relative("1 detik", "seconds", 1), "dalam 1 detik" - ) + assert self.locale._format_relative("1 jam", "hour", 1) == "dalam 1 jam" + assert self.locale._format_relative("1 detik", "seconds", 1) == "dalam 1 detik" def test_format_relative_future(self): - self.assertEqual( - self.locale._format_relative("1 jam", "hour", -1), "1 jam yang lalu" - ) - + assert self.locale._format_relative("1 jam", "hour", -1) == "1 jam yang lalu" -class TagalogLocaleTests(Chai): - def setUp(self): - super(TagalogLocaleTests, self).setUp() - self.locale = locales.TagalogLocale() +class TestTagalogLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.TagalogLocale() def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("minute", 1), "isang minuto") - self.assertEqual(self.locale._format_timeframe("hour", 1), "isang oras") - self.assertEqual(self.locale._format_timeframe("month", 1), "isang buwan") - self.assertEqual(self.locale._format_timeframe("year", 1), "isang taon") + assert self.locale._format_timeframe("minute", 1) == "isang minuto" + assert self.locale._format_timeframe("hour", 1) == "isang oras" + assert self.locale._format_timeframe("month", 1) == "isang buwan" + assert self.locale._format_timeframe("year", 1) == "isang taon" - self.assertEqual(self.locale._format_timeframe("seconds", 2), "2 segundo") - self.assertEqual(self.locale._format_timeframe("minutes", 3), "3 minuto") - self.assertEqual(self.locale._format_timeframe("hours", 4), "4 oras") - self.assertEqual(self.locale._format_timeframe("months", 5), "5 buwan") - self.assertEqual(self.locale._format_timeframe("years", 6), "6 taon") + assert self.locale._format_timeframe("seconds", 2) == "2 segundo" + assert self.locale._format_timeframe("minutes", 3) == "3 minuto" + assert self.locale._format_timeframe("hours", 4) == "4 oras" + assert self.locale._format_timeframe("months", 5) == "5 buwan" + assert self.locale._format_timeframe("years", 6) == "6 taon" def test_format_relative_now(self): - self.assertEqual( - self.locale._format_relative("ngayon lang", "now", 0), "ngayon lang" - ) + assert self.locale._format_relative("ngayon lang", "now", 0) == "ngayon lang" def test_format_relative_past(self): - self.assertEqual( - self.locale._format_relative("2 oras", "hour", 2), "2 oras mula ngayon" - ) + assert self.locale._format_relative("2 oras", "hour", 2) == "2 oras mula ngayon" def test_format_relative_future(self): - self.assertEqual( - self.locale._format_relative("3 oras", "hour", -3), "nakaraang 3 oras" - ) + assert self.locale._format_relative("3 oras", "hour", -3) == "nakaraang 3 oras" def test_ordinal_number(self): - self.assertEqual(self.locale.ordinal_number(0), "ika-0") - self.assertEqual(self.locale.ordinal_number(1), "ika-1") - self.assertEqual(self.locale.ordinal_number(2), "ika-2") - self.assertEqual(self.locale.ordinal_number(3), "ika-3") - self.assertEqual(self.locale.ordinal_number(10), "ika-10") - self.assertEqual(self.locale.ordinal_number(23), "ika-23") - self.assertEqual(self.locale.ordinal_number(100), "ika-100") - self.assertEqual(self.locale.ordinal_number(103), "ika-103") - self.assertEqual(self.locale.ordinal_number(114), "ika-114") - - -class EstonianLocaleTests(Chai): - def setUp(self): - super(EstonianLocaleTests, self).setUp() - - self.locale = locales.EstonianLocale() + assert self.locale.ordinal_number(0) == "ika-0" + assert self.locale.ordinal_number(1) == "ika-1" + assert self.locale.ordinal_number(2) == "ika-2" + assert self.locale.ordinal_number(3) == "ika-3" + assert self.locale.ordinal_number(10) == "ika-10" + assert self.locale.ordinal_number(23) == "ika-23" + assert self.locale.ordinal_number(100) == "ika-100" + assert self.locale.ordinal_number(103) == "ika-103" + assert self.locale.ordinal_number(114) == "ika-114" + + +class TestEstonianLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.EstonianLocale() def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("now", 0), "just nüüd") - self.assertEqual(self.locale._format_timeframe("second", 1), "ühe sekundi") - self.assertEqual(self.locale._format_timeframe("seconds", 3), "3 sekundi") - self.assertEqual(self.locale._format_timeframe("seconds", 30), "30 sekundi") - self.assertEqual(self.locale._format_timeframe("minute", 1), "ühe minuti") - self.assertEqual(self.locale._format_timeframe("minutes", 4), "4 minuti") - self.assertEqual(self.locale._format_timeframe("minutes", 40), "40 minuti") - self.assertEqual(self.locale._format_timeframe("hour", 1), "tunni aja") - self.assertEqual(self.locale._format_timeframe("hours", 5), "5 tunni") - self.assertEqual(self.locale._format_timeframe("hours", 23), "23 tunni") - self.assertEqual(self.locale._format_timeframe("day", 1), "ühe päeva") - self.assertEqual(self.locale._format_timeframe("days", 6), "6 päeva") - self.assertEqual(self.locale._format_timeframe("days", 12), "12 päeva") - self.assertEqual(self.locale._format_timeframe("month", 1), "ühe kuu") - self.assertEqual(self.locale._format_timeframe("months", 7), "7 kuu") - self.assertEqual(self.locale._format_timeframe("months", 11), "11 kuu") - self.assertEqual(self.locale._format_timeframe("year", 1), "ühe aasta") - self.assertEqual(self.locale._format_timeframe("years", 8), "8 aasta") - self.assertEqual(self.locale._format_timeframe("years", 12), "12 aasta") - - self.assertEqual(self.locale._format_timeframe("now", 0), "just nüüd") - self.assertEqual(self.locale._format_timeframe("second", -1), "üks sekund") - self.assertEqual(self.locale._format_timeframe("seconds", -9), "9 sekundit") - self.assertEqual(self.locale._format_timeframe("seconds", -12), "12 sekundit") - self.assertEqual(self.locale._format_timeframe("minute", -1), "üks minut") - self.assertEqual(self.locale._format_timeframe("minutes", -2), "2 minutit") - self.assertEqual(self.locale._format_timeframe("minutes", -10), "10 minutit") - self.assertEqual(self.locale._format_timeframe("hour", -1), "tund aega") - self.assertEqual(self.locale._format_timeframe("hours", -3), "3 tundi") - self.assertEqual(self.locale._format_timeframe("hours", -11), "11 tundi") - self.assertEqual(self.locale._format_timeframe("day", -1), "üks päev") - self.assertEqual(self.locale._format_timeframe("days", -2), "2 päeva") - self.assertEqual(self.locale._format_timeframe("days", -12), "12 päeva") - self.assertEqual(self.locale._format_timeframe("month", -1), "üks kuu") - self.assertEqual(self.locale._format_timeframe("months", -3), "3 kuud") - self.assertEqual(self.locale._format_timeframe("months", -13), "13 kuud") - self.assertEqual(self.locale._format_timeframe("year", -1), "üks aasta") - self.assertEqual(self.locale._format_timeframe("years", -4), "4 aastat") - self.assertEqual(self.locale._format_timeframe("years", -14), "14 aastat") - - -class PortugueseLocaleTests(Chai): - def setUp(self): - super(PortugueseLocaleTests, self).setUp() - - self.locale = locales.PortugueseLocale() + assert self.locale._format_timeframe("now", 0) == "just nüüd" + assert self.locale._format_timeframe("second", 1) == "ühe sekundi" + assert self.locale._format_timeframe("seconds", 3) == "3 sekundi" + assert self.locale._format_timeframe("seconds", 30) == "30 sekundi" + assert self.locale._format_timeframe("minute", 1) == "ühe minuti" + assert self.locale._format_timeframe("minutes", 4) == "4 minuti" + assert self.locale._format_timeframe("minutes", 40) == "40 minuti" + assert self.locale._format_timeframe("hour", 1) == "tunni aja" + assert self.locale._format_timeframe("hours", 5) == "5 tunni" + assert self.locale._format_timeframe("hours", 23) == "23 tunni" + assert self.locale._format_timeframe("day", 1) == "ühe päeva" + assert self.locale._format_timeframe("days", 6) == "6 päeva" + assert self.locale._format_timeframe("days", 12) == "12 päeva" + assert self.locale._format_timeframe("month", 1) == "ühe kuu" + assert self.locale._format_timeframe("months", 7) == "7 kuu" + assert self.locale._format_timeframe("months", 11) == "11 kuu" + assert self.locale._format_timeframe("year", 1) == "ühe aasta" + assert self.locale._format_timeframe("years", 8) == "8 aasta" + assert self.locale._format_timeframe("years", 12) == "12 aasta" + + assert self.locale._format_timeframe("now", 0) == "just nüüd" + assert self.locale._format_timeframe("second", -1) == "üks sekund" + assert self.locale._format_timeframe("seconds", -9) == "9 sekundit" + assert self.locale._format_timeframe("seconds", -12) == "12 sekundit" + assert self.locale._format_timeframe("minute", -1) == "üks minut" + assert self.locale._format_timeframe("minutes", -2) == "2 minutit" + assert self.locale._format_timeframe("minutes", -10) == "10 minutit" + assert self.locale._format_timeframe("hour", -1) == "tund aega" + assert self.locale._format_timeframe("hours", -3) == "3 tundi" + assert self.locale._format_timeframe("hours", -11) == "11 tundi" + assert self.locale._format_timeframe("day", -1) == "üks päev" + assert self.locale._format_timeframe("days", -2) == "2 päeva" + assert self.locale._format_timeframe("days", -12) == "12 päeva" + assert self.locale._format_timeframe("month", -1) == "üks kuu" + assert self.locale._format_timeframe("months", -3) == "3 kuud" + assert self.locale._format_timeframe("months", -13) == "13 kuud" + assert self.locale._format_timeframe("year", -1) == "üks aasta" + assert self.locale._format_timeframe("years", -4) == "4 aastat" + assert self.locale._format_timeframe("years", -14) == "14 aastat" + + +class TestPortugueseLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.PortugueseLocale() def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("now", 0), "agora") - self.assertEqual(self.locale._format_timeframe("second", 1), "um segundo") - self.assertEqual(self.locale._format_timeframe("seconds", 30), "30 segundos") - self.assertEqual(self.locale._format_timeframe("minute", 1), "um minuto") - self.assertEqual(self.locale._format_timeframe("minutes", 40), "40 minutos") - self.assertEqual(self.locale._format_timeframe("hour", 1), "uma hora") - self.assertEqual(self.locale._format_timeframe("hours", 23), "23 horas") - self.assertEqual(self.locale._format_timeframe("day", 1), "um dia") - self.assertEqual(self.locale._format_timeframe("days", 12), "12 dias") - self.assertEqual(self.locale._format_timeframe("month", 1), "um mês") - self.assertEqual(self.locale._format_timeframe("months", 11), "11 meses") - self.assertEqual(self.locale._format_timeframe("year", 1), "um ano") - self.assertEqual(self.locale._format_timeframe("years", 12), "12 anos") - - -class BrazilianLocaleTests(Chai): - def setUp(self): - super(BrazilianLocaleTests, self).setUp() - - self.locale = locales.BrazilianPortugueseLocale() + assert self.locale._format_timeframe("now", 0) == "agora" + assert self.locale._format_timeframe("second", 1) == "um segundo" + assert self.locale._format_timeframe("seconds", 30) == "30 segundos" + assert self.locale._format_timeframe("minute", 1) == "um minuto" + assert self.locale._format_timeframe("minutes", 40) == "40 minutos" + assert self.locale._format_timeframe("hour", 1) == "uma hora" + assert self.locale._format_timeframe("hours", 23) == "23 horas" + assert self.locale._format_timeframe("day", 1) == "um dia" + assert self.locale._format_timeframe("days", 12) == "12 dias" + assert self.locale._format_timeframe("month", 1) == "um mês" + assert self.locale._format_timeframe("months", 11) == "11 meses" + assert self.locale._format_timeframe("year", 1) == "um ano" + assert self.locale._format_timeframe("years", 12) == "12 anos" + + +class TestBrazilianLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.BrazilianPortugueseLocale() def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("now", 0), "agora") - self.assertEqual(self.locale._format_timeframe("second", 1), "um segundo") - self.assertEqual(self.locale._format_timeframe("seconds", 30), "30 segundos") - self.assertEqual(self.locale._format_timeframe("minute", 1), "um minuto") - self.assertEqual(self.locale._format_timeframe("minutes", 40), "40 minutos") - self.assertEqual(self.locale._format_timeframe("hour", 1), "uma hora") - self.assertEqual(self.locale._format_timeframe("hours", 23), "23 horas") - self.assertEqual(self.locale._format_timeframe("day", 1), "um dia") - self.assertEqual(self.locale._format_timeframe("days", 12), "12 dias") - self.assertEqual(self.locale._format_timeframe("month", 1), "um mês") - self.assertEqual(self.locale._format_timeframe("months", 11), "11 meses") - self.assertEqual(self.locale._format_timeframe("year", 1), "um ano") - self.assertEqual(self.locale._format_timeframe("years", 12), "12 anos") - - -class HongKongLocaleTests(Chai): - def setUp(self): - super(HongKongLocaleTests, self).setUp() - - self.locale = locales.HongKongLocale() + assert self.locale._format_timeframe("now", 0) == "agora" + assert self.locale._format_timeframe("second", 1) == "um segundo" + assert self.locale._format_timeframe("seconds", 30) == "30 segundos" + assert self.locale._format_timeframe("minute", 1) == "um minuto" + assert self.locale._format_timeframe("minutes", 40) == "40 minutos" + assert self.locale._format_timeframe("hour", 1) == "uma hora" + assert self.locale._format_timeframe("hours", 23) == "23 horas" + assert self.locale._format_timeframe("day", 1) == "um dia" + assert self.locale._format_timeframe("days", 12) == "12 dias" + assert self.locale._format_timeframe("month", 1) == "um mês" + assert self.locale._format_timeframe("months", 11) == "11 meses" + assert self.locale._format_timeframe("year", 1) == "um ano" + assert self.locale._format_timeframe("years", 12) == "12 anos" + + +class TestHongKongLocale: + @classmethod + def setup_class(cls): + cls.locale = locales.HongKongLocale() def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("now", 0), "剛才") - self.assertEqual(self.locale._format_timeframe("second", 1), "1秒") - self.assertEqual(self.locale._format_timeframe("seconds", 30), "30秒") - self.assertEqual(self.locale._format_timeframe("minute", 1), "1分鐘") - self.assertEqual(self.locale._format_timeframe("minutes", 40), "40分鐘") - self.assertEqual(self.locale._format_timeframe("hour", 1), "1小時") - self.assertEqual(self.locale._format_timeframe("hours", 23), "23小時") - self.assertEqual(self.locale._format_timeframe("day", 1), "1天") - self.assertEqual(self.locale._format_timeframe("days", 12), "12天") - self.assertEqual(self.locale._format_timeframe("week", 1), "1星期") - self.assertEqual(self.locale._format_timeframe("weeks", 38), "38星期") - self.assertEqual(self.locale._format_timeframe("month", 1), "1個月") - self.assertEqual(self.locale._format_timeframe("months", 11), "11個月") - self.assertEqual(self.locale._format_timeframe("year", 1), "1年") - self.assertEqual(self.locale._format_timeframe("years", 12), "12年") + assert self.locale._format_timeframe("now", 0) == "剛才" + assert self.locale._format_timeframe("second", 1) == "1秒" + assert self.locale._format_timeframe("seconds", 30) == "30秒" + assert self.locale._format_timeframe("minute", 1) == "1分鐘" + assert self.locale._format_timeframe("minutes", 40) == "40分鐘" + assert self.locale._format_timeframe("hour", 1) == "1小時" + assert self.locale._format_timeframe("hours", 23) == "23小時" + assert self.locale._format_timeframe("day", 1) == "1天" + assert self.locale._format_timeframe("days", 12) == "12天" + assert self.locale._format_timeframe("week", 1) == "1星期" + assert self.locale._format_timeframe("weeks", 38) == "38星期" + assert self.locale._format_timeframe("month", 1) == "1個月" + assert self.locale._format_timeframe("months", 11) == "11個月" + assert self.locale._format_timeframe("year", 1) == "1年" + assert self.locale._format_timeframe("years", 12) == "12年" diff --git a/tests/locales_tests.py.bak b/tests/locales_tests.py.bak new file mode 100644 index 000000000..af205b435 --- /dev/null +++ b/tests/locales_tests.py.bak @@ -0,0 +1,954 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from chai import Chai + +from arrow import arrow, locales + + +class LocaleValidationTests(Chai): + """Validate locales to ensure that translations are valid and complete""" + + def setUp(self): + super(LocaleValidationTests, self).setUp() + + self.locales = locales._locales + + def test_locale_validation(self): + + for _, locale_cls in self.locales.items(): + # 7 days + 1 spacer to allow for 1-indexing of months + self.assertEqual(len(locale_cls.day_names), 8) + self.assertTrue(locale_cls.day_names[0] == "") + # ensure that all string from index 1 onward are valid (not blank or None) + self.assertTrue(all(locale_cls.day_names[1:])) + + self.assertEqual(len(locale_cls.day_abbreviations), 8) + self.assertTrue(locale_cls.day_abbreviations[0] == "") + self.assertTrue(all(locale_cls.day_abbreviations[1:])) + + # 12 months + 1 spacer to allow for 1-indexing of months + self.assertEqual(len(locale_cls.month_names), 13) + self.assertTrue(locale_cls.month_names[0] == "") + self.assertTrue(all(locale_cls.month_names[1:])) + + self.assertEqual(len(locale_cls.month_abbreviations), 13) + self.assertTrue(locale_cls.month_abbreviations[0] == "") + self.assertTrue(all(locale_cls.month_abbreviations[1:])) + + self.assertTrue(len(locale_cls.names) > 0) + self.assertTrue(locale_cls.past is not None) + self.assertTrue(locale_cls.future is not None) + + +class ModuleTests(Chai): + def test_get_locale(self): + + mock_locales = self.mock(locales, "_locales") + mock_locale_cls = self.mock() + mock_locale = self.mock() + + self.expect(mock_locales.get).args("name").returns(mock_locale_cls) + self.expect(mock_locale_cls).returns(mock_locale) + + result = locales.get_locale("name") + + self.assertEqual(result, mock_locale) + + def test_locales(self): + + self.assertTrue(len(locales._locales) > 0) + + +class LocaleTests(Chai): + def setUp(self): + super(LocaleTests, self).setUp() + + self.locale = locales.EnglishLocale() + + def test_format_timeframe(self): + + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 hours") + self.assertEqual(self.locale._format_timeframe("hour", 0), "an hour") + + def test_format_relative_now(self): + + result = self.locale._format_relative("just now", "now", 0) + + self.assertEqual(result, "just now") + + def test_format_relative_past(self): + + result = self.locale._format_relative("an hour", "hour", 1) + + self.assertEqual(result, "in an hour") + + def test_format_relative_future(self): + + result = self.locale._format_relative("an hour", "hour", -1) + + self.assertEqual(result, "an hour ago") + + def test_ordinal_number(self): + self.assertEqual(self.locale.ordinal_number(0), "0th") + self.assertEqual(self.locale.ordinal_number(1), "1st") + self.assertEqual(self.locale.ordinal_number(2), "2nd") + self.assertEqual(self.locale.ordinal_number(3), "3rd") + self.assertEqual(self.locale.ordinal_number(4), "4th") + self.assertEqual(self.locale.ordinal_number(10), "10th") + self.assertEqual(self.locale.ordinal_number(11), "11th") + self.assertEqual(self.locale.ordinal_number(12), "12th") + self.assertEqual(self.locale.ordinal_number(13), "13th") + self.assertEqual(self.locale.ordinal_number(14), "14th") + self.assertEqual(self.locale.ordinal_number(21), "21st") + self.assertEqual(self.locale.ordinal_number(22), "22nd") + self.assertEqual(self.locale.ordinal_number(23), "23rd") + self.assertEqual(self.locale.ordinal_number(24), "24th") + + self.assertEqual(self.locale.ordinal_number(100), "100th") + self.assertEqual(self.locale.ordinal_number(101), "101st") + self.assertEqual(self.locale.ordinal_number(102), "102nd") + self.assertEqual(self.locale.ordinal_number(103), "103rd") + self.assertEqual(self.locale.ordinal_number(104), "104th") + self.assertEqual(self.locale.ordinal_number(110), "110th") + self.assertEqual(self.locale.ordinal_number(111), "111th") + self.assertEqual(self.locale.ordinal_number(112), "112th") + self.assertEqual(self.locale.ordinal_number(113), "113th") + self.assertEqual(self.locale.ordinal_number(114), "114th") + self.assertEqual(self.locale.ordinal_number(121), "121st") + self.assertEqual(self.locale.ordinal_number(122), "122nd") + self.assertEqual(self.locale.ordinal_number(123), "123rd") + self.assertEqual(self.locale.ordinal_number(124), "124th") + + def test_meridian_invalid_token(self): + self.assertEqual(self.locale.meridian(7, None), None) + self.assertEqual(self.locale.meridian(7, "B"), None) + self.assertEqual(self.locale.meridian(7, "NONSENSE"), None) + + +class EnglishLocaleTests(Chai): + def setUp(self): + super(EnglishLocaleTests, self).setUp() + + self.locale = locales.EnglishLocale() + + def test_describe(self): + self.assertEqual(self.locale.describe("now", only_distance=True), "instantly") + self.assertEqual(self.locale.describe("now", only_distance=False), "just now") + + +class ItalianLocalesTests(Chai): + def test_ordinal_number(self): + locale = locales.ItalianLocale() + + self.assertEqual(locale.ordinal_number(1), "1º") + + +class SpanishLocalesTests(Chai): + def test_ordinal_number(self): + locale = locales.SpanishLocale() + + self.assertEqual(locale.ordinal_number(1), "1º") + + def test_format_timeframe(self): + locale = locales.SpanishLocale() + self.assertEqual(locale._format_timeframe("now", 0), "ahora") + self.assertEqual(locale._format_timeframe("seconds", 1), "1 segundos") + self.assertEqual(locale._format_timeframe("seconds", 3), "3 segundos") + self.assertEqual(locale._format_timeframe("seconds", 30), "30 segundos") + self.assertEqual(locale._format_timeframe("minute", 1), "un minuto") + self.assertEqual(locale._format_timeframe("minutes", 4), "4 minutos") + self.assertEqual(locale._format_timeframe("minutes", 40), "40 minutos") + self.assertEqual(locale._format_timeframe("hour", 1), "una hora") + self.assertEqual(locale._format_timeframe("hours", 5), "5 horas") + self.assertEqual(locale._format_timeframe("hours", 23), "23 horas") + self.assertEqual(locale._format_timeframe("day", 1), "un día") + self.assertEqual(locale._format_timeframe("days", 6), "6 días") + self.assertEqual(locale._format_timeframe("days", 12), "12 días") + self.assertEqual(locale._format_timeframe("week", 1), "una semana") + self.assertEqual(locale._format_timeframe("weeks", 2), "2 semanas") + self.assertEqual(locale._format_timeframe("weeks", 3), "3 semanas") + self.assertEqual(locale._format_timeframe("month", 1), "un mes") + self.assertEqual(locale._format_timeframe("months", 7), "7 meses") + self.assertEqual(locale._format_timeframe("months", 11), "11 meses") + self.assertEqual(locale._format_timeframe("year", 1), "un año") + self.assertEqual(locale._format_timeframe("years", 8), "8 años") + self.assertEqual(locale._format_timeframe("years", 12), "12 años") + + self.assertEqual(locale._format_timeframe("now", 0), "ahora") + self.assertEqual(locale._format_timeframe("seconds", -1), "1 segundos") + self.assertEqual(locale._format_timeframe("seconds", -9), "9 segundos") + self.assertEqual(locale._format_timeframe("seconds", -12), "12 segundos") + self.assertEqual(locale._format_timeframe("minute", -1), "un minuto") + self.assertEqual(locale._format_timeframe("minutes", -2), "2 minutos") + self.assertEqual(locale._format_timeframe("minutes", -10), "10 minutos") + self.assertEqual(locale._format_timeframe("hour", -1), "una hora") + self.assertEqual(locale._format_timeframe("hours", -3), "3 horas") + self.assertEqual(locale._format_timeframe("hours", -11), "11 horas") + self.assertEqual(locale._format_timeframe("day", -1), "un día") + self.assertEqual(locale._format_timeframe("days", -2), "2 días") + self.assertEqual(locale._format_timeframe("days", -12), "12 días") + self.assertEqual(locale._format_timeframe("week", -1), "una semana") + self.assertEqual(locale._format_timeframe("weeks", -2), "2 semanas") + self.assertEqual(locale._format_timeframe("weeks", -3), "3 semanas") + self.assertEqual(locale._format_timeframe("month", -1), "un mes") + self.assertEqual(locale._format_timeframe("months", -3), "3 meses") + self.assertEqual(locale._format_timeframe("months", -13), "13 meses") + self.assertEqual(locale._format_timeframe("year", -1), "un año") + self.assertEqual(locale._format_timeframe("years", -4), "4 años") + self.assertEqual(locale._format_timeframe("years", -14), "14 años") + + +class FrenchLocalesTests(Chai): + def test_ordinal_number(self): + locale = locales.FrenchLocale() + + self.assertEqual(locale.ordinal_number(1), "1er") + self.assertEqual(locale.ordinal_number(2), "2e") + + +class RussianLocalesTests(Chai): + def test_plurals2(self): + + locale = locales.RussianLocale() + + self.assertEqual(locale._format_timeframe("hours", 0), "0 часов") + self.assertEqual(locale._format_timeframe("hours", 1), "1 час") + self.assertEqual(locale._format_timeframe("hours", 2), "2 часа") + self.assertEqual(locale._format_timeframe("hours", 4), "4 часа") + self.assertEqual(locale._format_timeframe("hours", 5), "5 часов") + self.assertEqual(locale._format_timeframe("hours", 21), "21 час") + self.assertEqual(locale._format_timeframe("hours", 22), "22 часа") + self.assertEqual(locale._format_timeframe("hours", 25), "25 часов") + + # feminine grammatical gender should be tested separately + self.assertEqual(locale._format_timeframe("minutes", 0), "0 минут") + self.assertEqual(locale._format_timeframe("minutes", 1), "1 минуту") + self.assertEqual(locale._format_timeframe("minutes", 2), "2 минуты") + self.assertEqual(locale._format_timeframe("minutes", 4), "4 минуты") + self.assertEqual(locale._format_timeframe("minutes", 5), "5 минут") + self.assertEqual(locale._format_timeframe("minutes", 21), "21 минуту") + self.assertEqual(locale._format_timeframe("minutes", 22), "22 минуты") + self.assertEqual(locale._format_timeframe("minutes", 25), "25 минут") + + +class PolishLocalesTests(Chai): + def test_plurals(self): + + locale = locales.PolishLocale() + + self.assertEqual(locale._format_timeframe("hours", 0), "0 godzin") + self.assertEqual(locale._format_timeframe("hours", 1), "1 godzin") + self.assertEqual(locale._format_timeframe("hours", 2), "2 godziny") + self.assertEqual(locale._format_timeframe("hours", 4), "4 godziny") + self.assertEqual(locale._format_timeframe("hours", 5), "5 godzin") + self.assertEqual(locale._format_timeframe("hours", 21), "21 godzin") + self.assertEqual(locale._format_timeframe("hours", 22), "22 godziny") + self.assertEqual(locale._format_timeframe("hours", 25), "25 godzin") + + +class IcelandicLocalesTests(Chai): + def setUp(self): + super(IcelandicLocalesTests, self).setUp() + + self.locale = locales.IcelandicLocale() + + def test_format_timeframe(self): + + self.assertEqual(self.locale._format_timeframe("minute", -1), "einni mínútu") + self.assertEqual(self.locale._format_timeframe("minute", 1), "eina mínútu") + + self.assertEqual(self.locale._format_timeframe("hours", -2), "2 tímum") + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 tíma") + self.assertEqual(self.locale._format_timeframe("now", 0), "rétt í þessu") + + +class MalayalamLocaleTests(Chai): + def setUp(self): + super(MalayalamLocaleTests, self).setUp() + + self.locale = locales.MalayalamLocale() + + def test_format_timeframe(self): + + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 മണിക്കൂർ") + self.assertEqual(self.locale._format_timeframe("hour", 0), "ഒരു മണിക്കൂർ") + + def test_format_relative_now(self): + + result = self.locale._format_relative("ഇപ്പോൾ", "now", 0) + + self.assertEqual(result, "ഇപ്പോൾ") + + def test_format_relative_past(self): + + result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", 1) + self.assertEqual(result, "ഒരു മണിക്കൂർ ശേഷം") + + def test_format_relative_future(self): + + result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", -1) + self.assertEqual(result, "ഒരു മണിക്കൂർ മുമ്പ്") + + +class HindiLocaleTests(Chai): + def setUp(self): + super(HindiLocaleTests, self).setUp() + + self.locale = locales.HindiLocale() + + def test_format_timeframe(self): + + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 घंटे") + self.assertEqual(self.locale._format_timeframe("hour", 0), "एक घंटा") + + def test_format_relative_now(self): + + result = self.locale._format_relative("अभी", "now", 0) + + self.assertEqual(result, "अभी") + + def test_format_relative_past(self): + + result = self.locale._format_relative("एक घंटा", "hour", 1) + self.assertEqual(result, "एक घंटा बाद") + + def test_format_relative_future(self): + + result = self.locale._format_relative("एक घंटा", "hour", -1) + self.assertEqual(result, "एक घंटा पहले") + + +class CzechLocaleTests(Chai): + def setUp(self): + super(CzechLocaleTests, self).setUp() + + self.locale = locales.CzechLocale() + + def test_format_timeframe(self): + + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 hodiny") + self.assertEqual(self.locale._format_timeframe("hours", 5), "5 hodin") + self.assertEqual(self.locale._format_timeframe("hour", 0), "0 hodin") + self.assertEqual(self.locale._format_timeframe("hours", -2), "2 hodinami") + self.assertEqual(self.locale._format_timeframe("hours", -5), "5 hodinami") + self.assertEqual(self.locale._format_timeframe("now", 0), "Teď") + + def test_format_relative_now(self): + + result = self.locale._format_relative("Teď", "now", 0) + self.assertEqual(result, "Teď") + + def test_format_relative_future(self): + + result = self.locale._format_relative("hodinu", "hour", 1) + self.assertEqual(result, "Za hodinu") + + def test_format_relative_past(self): + + result = self.locale._format_relative("hodinou", "hour", -1) + self.assertEqual(result, "Před hodinou") + + +class SlovakLocaleTests(Chai): + def setUp(self): + super(SlovakLocaleTests, self).setUp() + + self.locale = locales.SlovakLocale() + + def test_format_timeframe(self): + + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 hodiny") + self.assertEqual(self.locale._format_timeframe("hours", 5), "5 hodín") + self.assertEqual(self.locale._format_timeframe("hour", 0), "0 hodín") + self.assertEqual(self.locale._format_timeframe("hours", -2), "2 hodinami") + self.assertEqual(self.locale._format_timeframe("hours", -5), "5 hodinami") + self.assertEqual(self.locale._format_timeframe("now", 0), "Teraz") + + def test_format_relative_now(self): + + result = self.locale._format_relative("Teraz", "now", 0) + self.assertEqual(result, "Teraz") + + def test_format_relative_future(self): + + result = self.locale._format_relative("hodinu", "hour", 1) + self.assertEqual(result, "O hodinu") + + def test_format_relative_past(self): + + result = self.locale._format_relative("hodinou", "hour", -1) + self.assertEqual(result, "Pred hodinou") + + +class BulgarianLocaleTests(Chai): + def test_plurals2(self): + + locale = locales.BulgarianLocale() + + self.assertEqual(locale._format_timeframe("hours", 0), "0 часа") + self.assertEqual(locale._format_timeframe("hours", 1), "1 час") + self.assertEqual(locale._format_timeframe("hours", 2), "2 часа") + self.assertEqual(locale._format_timeframe("hours", 4), "4 часа") + self.assertEqual(locale._format_timeframe("hours", 5), "5 часа") + self.assertEqual(locale._format_timeframe("hours", 21), "21 час") + self.assertEqual(locale._format_timeframe("hours", 22), "22 часа") + self.assertEqual(locale._format_timeframe("hours", 25), "25 часа") + + # feminine grammatical gender should be tested separately + self.assertEqual(locale._format_timeframe("minutes", 0), "0 минути") + self.assertEqual(locale._format_timeframe("minutes", 1), "1 минута") + self.assertEqual(locale._format_timeframe("minutes", 2), "2 минути") + self.assertEqual(locale._format_timeframe("minutes", 4), "4 минути") + self.assertEqual(locale._format_timeframe("minutes", 5), "5 минути") + self.assertEqual(locale._format_timeframe("minutes", 21), "21 минута") + self.assertEqual(locale._format_timeframe("minutes", 22), "22 минути") + self.assertEqual(locale._format_timeframe("minutes", 25), "25 минути") + + +class MacedonianLocaleTests(Chai): + def test_plurals_mk(self): + + locale = locales.MacedonianLocale() + + # time + self.assertEqual(locale._format_relative("сега", "now", 0), "сега") + + # Hours + self.assertEqual(locale._format_timeframe("hours", 0), "0 саати") + self.assertEqual(locale._format_timeframe("hours", 1), "1 саат") + self.assertEqual(locale._format_timeframe("hours", 2), "2 саати") + self.assertEqual(locale._format_timeframe("hours", 4), "4 саати") + self.assertEqual(locale._format_timeframe("hours", 5), "5 саати") + self.assertEqual(locale._format_timeframe("hours", 21), "21 саат") + self.assertEqual(locale._format_timeframe("hours", 22), "22 саати") + self.assertEqual(locale._format_timeframe("hours", 25), "25 саати") + + # Minutes + self.assertEqual(locale._format_timeframe("minutes", 0), "0 минути") + self.assertEqual(locale._format_timeframe("minutes", 1), "1 минута") + self.assertEqual(locale._format_timeframe("minutes", 2), "2 минути") + self.assertEqual(locale._format_timeframe("minutes", 4), "4 минути") + self.assertEqual(locale._format_timeframe("minutes", 5), "5 минути") + self.assertEqual(locale._format_timeframe("minutes", 21), "21 минута") + self.assertEqual(locale._format_timeframe("minutes", 22), "22 минути") + self.assertEqual(locale._format_timeframe("minutes", 25), "25 минути") + + +class HebrewLocaleTests(Chai): + def test_couple_of_timeframe(self): + locale = locales.HebrewLocale() + + self.assertEqual(locale._format_timeframe("hours", 2), "שעתיים") + self.assertEqual(locale._format_timeframe("months", 2), "חודשיים") + self.assertEqual(locale._format_timeframe("days", 2), "יומיים") + self.assertEqual(locale._format_timeframe("years", 2), "שנתיים") + + self.assertEqual(locale._format_timeframe("hours", 3), "3 שעות") + self.assertEqual(locale._format_timeframe("months", 4), "4 חודשים") + self.assertEqual(locale._format_timeframe("days", 3), "3 ימים") + self.assertEqual(locale._format_timeframe("years", 5), "5 שנים") + + +class MarathiLocaleTests(Chai): + def setUp(self): + super(MarathiLocaleTests, self).setUp() + + self.locale = locales.MarathiLocale() + + def test_dateCoreFunctionality(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + self.assertEqual(self.locale.month_name(dt.month), "एप्रिल") + self.assertEqual(self.locale.month_abbreviation(dt.month), "एप्रि") + self.assertEqual(self.locale.day_name(dt.isoweekday()), "शनिवार") + self.assertEqual(self.locale.day_abbreviation(dt.isoweekday()), "शनि") + + def test_format_timeframe(self): + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 तास") + self.assertEqual(self.locale._format_timeframe("hour", 0), "एक तास") + + def test_format_relative_now(self): + result = self.locale._format_relative("सद्य", "now", 0) + self.assertEqual(result, "सद्य") + + def test_format_relative_past(self): + result = self.locale._format_relative("एक तास", "hour", 1) + self.assertEqual(result, "एक तास नंतर") + + def test_format_relative_future(self): + result = self.locale._format_relative("एक तास", "hour", -1) + self.assertEqual(result, "एक तास आधी") + + # Not currently implemented + def test_ordinal_number(self): + self.assertEqual(self.locale.ordinal_number(1), "1") + + +class FinnishLocaleTests(Chai): + def setUp(self): + super(FinnishLocaleTests, self).setUp() + + self.locale = locales.FinnishLocale() + + def test_format_timeframe(self): + self.assertEqual( + self.locale._format_timeframe("hours", 2), ("2 tuntia", "2 tunnin") + ) + self.assertEqual(self.locale._format_timeframe("hour", 0), ("tunti", "tunnin")) + + def test_format_relative_now(self): + result = self.locale._format_relative(["juuri nyt", "juuri nyt"], "now", 0) + self.assertEqual(result, "juuri nyt") + + def test_format_relative_past(self): + result = self.locale._format_relative(["tunti", "tunnin"], "hour", 1) + self.assertEqual(result, "tunnin kuluttua") + + def test_format_relative_future(self): + result = self.locale._format_relative(["tunti", "tunnin"], "hour", -1) + self.assertEqual(result, "tunti sitten") + + def test_ordinal_number(self): + self.assertEqual(self.locale.ordinal_number(1), "1.") + + +class GermanLocaleTests(Chai): + def setUp(self): + super(GermanLocaleTests, self).setUp() + + self.locale = locales.GermanLocale() + + def test_ordinal_number(self): + self.assertEqual(self.locale.ordinal_number(1), "1.") + + def test_define(self): + self.assertEqual( + self.locale.describe("minute", only_distance=True), "eine Minute" + ) + self.assertEqual( + self.locale.describe("minute", only_distance=False), "in einer Minute" + ) + self.assertEqual( + self.locale.describe("hour", only_distance=True), "eine Stunde" + ) + self.assertEqual( + self.locale.describe("hour", only_distance=False), "in einer Stunde" + ) + self.assertEqual(self.locale.describe("day", only_distance=True), "ein Tag") + self.assertEqual( + self.locale.describe("day", only_distance=False), "in einem Tag" + ) + self.assertEqual(self.locale.describe("month", only_distance=True), "ein Monat") + self.assertEqual( + self.locale.describe("month", only_distance=False), "in einem Monat" + ) + self.assertEqual(self.locale.describe("year", only_distance=True), "ein Jahr") + self.assertEqual( + self.locale.describe("year", only_distance=False), "in einem Jahr" + ) + + +class HungarianLocaleTests(Chai): + def setUp(self): + super(HungarianLocaleTests, self).setUp() + + self.locale = locales.HungarianLocale() + + def test_format_timeframe(self): + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 óra") + self.assertEqual(self.locale._format_timeframe("hour", 0), "egy órával") + self.assertEqual(self.locale._format_timeframe("hours", -2), "2 órával") + self.assertEqual(self.locale._format_timeframe("now", 0), "éppen most") + + +class EsperantoLocaleTests(Chai): + def setUp(self): + super(EsperantoLocaleTests, self).setUp() + + self.locale = locales.EsperantoLocale() + + def test_format_timeframe(self): + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 horoj") + self.assertEqual(self.locale._format_timeframe("hour", 0), "un horo") + self.assertEqual(self.locale._format_timeframe("hours", -2), "2 horoj") + self.assertEqual(self.locale._format_timeframe("now", 0), "nun") + + def test_ordinal_number(self): + self.assertEqual(self.locale.ordinal_number(1), "1a") + + +class ThaiLocaleTests(Chai): + def setUp(self): + super(ThaiLocaleTests, self).setUp() + + self.locale = locales.ThaiLocale() + + def test_year_full(self): + self.assertEqual(self.locale.year_full(2015), "2558") + + def test_year_abbreviation(self): + self.assertEqual(self.locale.year_abbreviation(2015), "58") + + def test_format_relative_now(self): + result = self.locale._format_relative("ขณะนี้", "now", 0) + self.assertEqual(result, "ขณะนี้") + + def test_format_relative_past(self): + result = self.locale._format_relative("1 ชั่วโมง", "hour", 1) + self.assertEqual(result, "ในอีก 1 ชั่วโมง") + result = self.locale._format_relative("{0} ชั่วโมง", "hours", 2) + self.assertEqual(result, "ในอีก {0} ชั่วโมง") + result = self.locale._format_relative("ไม่กี่วินาที", "seconds", 42) + self.assertEqual(result, "ในอีกไม่กี่วินาที") + + def test_format_relative_future(self): + result = self.locale._format_relative("1 ชั่วโมง", "hour", -1) + self.assertEqual(result, "1 ชั่วโมง ที่ผ่านมา") + + +class BengaliLocaleTests(Chai): + def setUp(self): + super(BengaliLocaleTests, self).setUp() + + self.locale = locales.BengaliLocale() + + def test_ordinal_number(self): + result0 = self.locale._ordinal_number(0) + result1 = self.locale._ordinal_number(1) + result3 = self.locale._ordinal_number(3) + result4 = self.locale._ordinal_number(4) + result5 = self.locale._ordinal_number(5) + result6 = self.locale._ordinal_number(6) + result10 = self.locale._ordinal_number(10) + result11 = self.locale._ordinal_number(11) + result42 = self.locale._ordinal_number(42) + self.assertEqual(result0, "0তম") + self.assertEqual(result1, "1ম") + self.assertEqual(result3, "3য়") + self.assertEqual(result4, "4র্থ") + self.assertEqual(result5, "5ম") + self.assertEqual(result6, "6ষ্ঠ") + self.assertEqual(result10, "10ম") + self.assertEqual(result11, "11তম") + self.assertEqual(result42, "42তম") + self.assertEqual(self.locale._ordinal_number(-1), None) + + +class SwissLocaleTests(Chai): + def setUp(self): + super(SwissLocaleTests, self).setUp() + + self.locale = locales.SwissLocale() + + def test_ordinal_number(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + + self.assertEqual(self.locale._format_timeframe("minute", 1), "einer Minute") + self.assertEqual(self.locale._format_timeframe("hour", 1), "einer Stunde") + self.assertEqual(self.locale.day_abbreviation(dt.isoweekday()), "Sa") + + +class RomanianLocaleTests(Chai): + def setUp(self): + super(RomanianLocaleTests, self).setUp() + + self.locale = locales.RomanianLocale() + + def test_timeframes(self): + + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 ore") + self.assertEqual(self.locale._format_timeframe("months", 2), "2 luni") + + self.assertEqual(self.locale._format_timeframe("days", 2), "2 zile") + self.assertEqual(self.locale._format_timeframe("years", 2), "2 ani") + + self.assertEqual(self.locale._format_timeframe("hours", 3), "3 ore") + self.assertEqual(self.locale._format_timeframe("months", 4), "4 luni") + self.assertEqual(self.locale._format_timeframe("days", 3), "3 zile") + self.assertEqual(self.locale._format_timeframe("years", 5), "5 ani") + + def test_relative_timeframes(self): + self.assertEqual(self.locale._format_relative("acum", "now", 0), "acum") + self.assertEqual( + self.locale._format_relative("o oră", "hour", 1), "peste o oră" + ) + self.assertEqual( + self.locale._format_relative("o oră", "hour", -1), "o oră în urmă" + ) + self.assertEqual( + self.locale._format_relative("un minut", "minute", 1), "peste un minut" + ) + self.assertEqual( + self.locale._format_relative("un minut", "minute", -1), "un minut în urmă" + ) + self.assertEqual( + self.locale._format_relative("câteva secunde", "seconds", -1), + "câteva secunde în urmă", + ) + self.assertEqual( + self.locale._format_relative("câteva secunde", "seconds", 1), + "peste câteva secunde", + ) + self.assertEqual( + self.locale._format_relative("o zi", "day", -1), "o zi în urmă" + ) + self.assertEqual(self.locale._format_relative("o zi", "day", 1), "peste o zi") + + +class ArabicLocalesTest(Chai): + def setUp(self): + super(ArabicLocalesTest, self).setUp() + + self.locale = locales.ArabicLocale() + + def test_timeframes(self): + + # single + self.assertEqual(self.locale._format_timeframe("minute", 1), "دقيقة") + self.assertEqual(self.locale._format_timeframe("hour", 1), "ساعة") + self.assertEqual(self.locale._format_timeframe("day", 1), "يوم") + self.assertEqual(self.locale._format_timeframe("month", 1), "شهر") + self.assertEqual(self.locale._format_timeframe("year", 1), "سنة") + + # double + self.assertEqual(self.locale._format_timeframe("minutes", 2), "دقيقتين") + self.assertEqual(self.locale._format_timeframe("hours", 2), "ساعتين") + self.assertEqual(self.locale._format_timeframe("days", 2), "يومين") + self.assertEqual(self.locale._format_timeframe("months", 2), "شهرين") + self.assertEqual(self.locale._format_timeframe("years", 2), "سنتين") + + # up to ten + self.assertEqual(self.locale._format_timeframe("minutes", 3), "3 دقائق") + self.assertEqual(self.locale._format_timeframe("hours", 4), "4 ساعات") + self.assertEqual(self.locale._format_timeframe("days", 5), "5 أيام") + self.assertEqual(self.locale._format_timeframe("months", 6), "6 أشهر") + self.assertEqual(self.locale._format_timeframe("years", 10), "10 سنوات") + + # more than ten + self.assertEqual(self.locale._format_timeframe("minutes", 11), "11 دقيقة") + self.assertEqual(self.locale._format_timeframe("hours", 19), "19 ساعة") + self.assertEqual(self.locale._format_timeframe("months", 24), "24 شهر") + self.assertEqual(self.locale._format_timeframe("days", 50), "50 يوم") + self.assertEqual(self.locale._format_timeframe("years", 115), "115 سنة") + + +class NepaliLocaleTests(Chai): + def setUp(self): + super(NepaliLocaleTests, self).setUp() + + self.locale = locales.NepaliLocale() + + def test_format_timeframe(self): + self.assertEqual(self.locale._format_timeframe("hours", 3), "3 घण्टा") + self.assertEqual(self.locale._format_timeframe("hour", 0), "एक घण्टा") + + def test_format_relative_now(self): + result = self.locale._format_relative("अहिले", "now", 0) + self.assertEqual(result, "अहिले") + + def test_format_relative_future(self): + result = self.locale._format_relative("एक घण्टा", "hour", 1) + self.assertEqual(result, "एक घण्टा पछी") + + def test_format_relative_past(self): + result = self.locale._format_relative("एक घण्टा", "hour", -1) + self.assertEqual(result, "एक घण्टा पहिले") + + +class IndonesianLocaleTests(Chai): + def setUp(self): + super(IndonesianLocaleTests, self).setUp() + + self.locale = locales.IndonesianLocale() + + def test_timeframes(self): + self.assertEqual(self.locale._format_timeframe("hours", 2), "2 jam") + self.assertEqual(self.locale._format_timeframe("months", 2), "2 bulan") + + self.assertEqual(self.locale._format_timeframe("days", 2), "2 hari") + self.assertEqual(self.locale._format_timeframe("years", 2), "2 tahun") + + self.assertEqual(self.locale._format_timeframe("hours", 3), "3 jam") + self.assertEqual(self.locale._format_timeframe("months", 4), "4 bulan") + self.assertEqual(self.locale._format_timeframe("days", 3), "3 hari") + self.assertEqual(self.locale._format_timeframe("years", 5), "5 tahun") + + def test_format_relative_now(self): + self.assertEqual( + self.locale._format_relative("baru saja", "now", 0), "baru saja" + ) + + def test_format_relative_past(self): + self.assertEqual( + self.locale._format_relative("1 jam", "hour", 1), "dalam 1 jam" + ) + self.assertEqual( + self.locale._format_relative("1 detik", "seconds", 1), "dalam 1 detik" + ) + + def test_format_relative_future(self): + self.assertEqual( + self.locale._format_relative("1 jam", "hour", -1), "1 jam yang lalu" + ) + + +class TagalogLocaleTests(Chai): + def setUp(self): + super(TagalogLocaleTests, self).setUp() + + self.locale = locales.TagalogLocale() + + def test_format_timeframe(self): + + self.assertEqual(self.locale._format_timeframe("minute", 1), "isang minuto") + self.assertEqual(self.locale._format_timeframe("hour", 1), "isang oras") + self.assertEqual(self.locale._format_timeframe("month", 1), "isang buwan") + self.assertEqual(self.locale._format_timeframe("year", 1), "isang taon") + + self.assertEqual(self.locale._format_timeframe("seconds", 2), "2 segundo") + self.assertEqual(self.locale._format_timeframe("minutes", 3), "3 minuto") + self.assertEqual(self.locale._format_timeframe("hours", 4), "4 oras") + self.assertEqual(self.locale._format_timeframe("months", 5), "5 buwan") + self.assertEqual(self.locale._format_timeframe("years", 6), "6 taon") + + def test_format_relative_now(self): + self.assertEqual( + self.locale._format_relative("ngayon lang", "now", 0), "ngayon lang" + ) + + def test_format_relative_past(self): + self.assertEqual( + self.locale._format_relative("2 oras", "hour", 2), "2 oras mula ngayon" + ) + + def test_format_relative_future(self): + self.assertEqual( + self.locale._format_relative("3 oras", "hour", -3), "nakaraang 3 oras" + ) + + def test_ordinal_number(self): + self.assertEqual(self.locale.ordinal_number(0), "ika-0") + self.assertEqual(self.locale.ordinal_number(1), "ika-1") + self.assertEqual(self.locale.ordinal_number(2), "ika-2") + self.assertEqual(self.locale.ordinal_number(3), "ika-3") + self.assertEqual(self.locale.ordinal_number(10), "ika-10") + self.assertEqual(self.locale.ordinal_number(23), "ika-23") + self.assertEqual(self.locale.ordinal_number(100), "ika-100") + self.assertEqual(self.locale.ordinal_number(103), "ika-103") + self.assertEqual(self.locale.ordinal_number(114), "ika-114") + + +class EstonianLocaleTests(Chai): + def setUp(self): + super(EstonianLocaleTests, self).setUp() + + self.locale = locales.EstonianLocale() + + def test_format_timeframe(self): + self.assertEqual(self.locale._format_timeframe("now", 0), "just nüüd") + self.assertEqual(self.locale._format_timeframe("second", 1), "ühe sekundi") + self.assertEqual(self.locale._format_timeframe("seconds", 3), "3 sekundi") + self.assertEqual(self.locale._format_timeframe("seconds", 30), "30 sekundi") + self.assertEqual(self.locale._format_timeframe("minute", 1), "ühe minuti") + self.assertEqual(self.locale._format_timeframe("minutes", 4), "4 minuti") + self.assertEqual(self.locale._format_timeframe("minutes", 40), "40 minuti") + self.assertEqual(self.locale._format_timeframe("hour", 1), "tunni aja") + self.assertEqual(self.locale._format_timeframe("hours", 5), "5 tunni") + self.assertEqual(self.locale._format_timeframe("hours", 23), "23 tunni") + self.assertEqual(self.locale._format_timeframe("day", 1), "ühe päeva") + self.assertEqual(self.locale._format_timeframe("days", 6), "6 päeva") + self.assertEqual(self.locale._format_timeframe("days", 12), "12 päeva") + self.assertEqual(self.locale._format_timeframe("month", 1), "ühe kuu") + self.assertEqual(self.locale._format_timeframe("months", 7), "7 kuu") + self.assertEqual(self.locale._format_timeframe("months", 11), "11 kuu") + self.assertEqual(self.locale._format_timeframe("year", 1), "ühe aasta") + self.assertEqual(self.locale._format_timeframe("years", 8), "8 aasta") + self.assertEqual(self.locale._format_timeframe("years", 12), "12 aasta") + + self.assertEqual(self.locale._format_timeframe("now", 0), "just nüüd") + self.assertEqual(self.locale._format_timeframe("second", -1), "üks sekund") + self.assertEqual(self.locale._format_timeframe("seconds", -9), "9 sekundit") + self.assertEqual(self.locale._format_timeframe("seconds", -12), "12 sekundit") + self.assertEqual(self.locale._format_timeframe("minute", -1), "üks minut") + self.assertEqual(self.locale._format_timeframe("minutes", -2), "2 minutit") + self.assertEqual(self.locale._format_timeframe("minutes", -10), "10 minutit") + self.assertEqual(self.locale._format_timeframe("hour", -1), "tund aega") + self.assertEqual(self.locale._format_timeframe("hours", -3), "3 tundi") + self.assertEqual(self.locale._format_timeframe("hours", -11), "11 tundi") + self.assertEqual(self.locale._format_timeframe("day", -1), "üks päev") + self.assertEqual(self.locale._format_timeframe("days", -2), "2 päeva") + self.assertEqual(self.locale._format_timeframe("days", -12), "12 päeva") + self.assertEqual(self.locale._format_timeframe("month", -1), "üks kuu") + self.assertEqual(self.locale._format_timeframe("months", -3), "3 kuud") + self.assertEqual(self.locale._format_timeframe("months", -13), "13 kuud") + self.assertEqual(self.locale._format_timeframe("year", -1), "üks aasta") + self.assertEqual(self.locale._format_timeframe("years", -4), "4 aastat") + self.assertEqual(self.locale._format_timeframe("years", -14), "14 aastat") + + +class PortugueseLocaleTests(Chai): + def setUp(self): + super(PortugueseLocaleTests, self).setUp() + + self.locale = locales.PortugueseLocale() + + def test_format_timeframe(self): + self.assertEqual(self.locale._format_timeframe("now", 0), "agora") + self.assertEqual(self.locale._format_timeframe("second", 1), "um segundo") + self.assertEqual(self.locale._format_timeframe("seconds", 30), "30 segundos") + self.assertEqual(self.locale._format_timeframe("minute", 1), "um minuto") + self.assertEqual(self.locale._format_timeframe("minutes", 40), "40 minutos") + self.assertEqual(self.locale._format_timeframe("hour", 1), "uma hora") + self.assertEqual(self.locale._format_timeframe("hours", 23), "23 horas") + self.assertEqual(self.locale._format_timeframe("day", 1), "um dia") + self.assertEqual(self.locale._format_timeframe("days", 12), "12 dias") + self.assertEqual(self.locale._format_timeframe("month", 1), "um mês") + self.assertEqual(self.locale._format_timeframe("months", 11), "11 meses") + self.assertEqual(self.locale._format_timeframe("year", 1), "um ano") + self.assertEqual(self.locale._format_timeframe("years", 12), "12 anos") + + +class BrazilianLocaleTests(Chai): + def setUp(self): + super(BrazilianLocaleTests, self).setUp() + + self.locale = locales.BrazilianPortugueseLocale() + + def test_format_timeframe(self): + self.assertEqual(self.locale._format_timeframe("now", 0), "agora") + self.assertEqual(self.locale._format_timeframe("second", 1), "um segundo") + self.assertEqual(self.locale._format_timeframe("seconds", 30), "30 segundos") + self.assertEqual(self.locale._format_timeframe("minute", 1), "um minuto") + self.assertEqual(self.locale._format_timeframe("minutes", 40), "40 minutos") + self.assertEqual(self.locale._format_timeframe("hour", 1), "uma hora") + self.assertEqual(self.locale._format_timeframe("hours", 23), "23 horas") + self.assertEqual(self.locale._format_timeframe("day", 1), "um dia") + self.assertEqual(self.locale._format_timeframe("days", 12), "12 dias") + self.assertEqual(self.locale._format_timeframe("month", 1), "um mês") + self.assertEqual(self.locale._format_timeframe("months", 11), "11 meses") + self.assertEqual(self.locale._format_timeframe("year", 1), "um ano") + self.assertEqual(self.locale._format_timeframe("years", 12), "12 anos") + + +class HongKongLocaleTests(Chai): + def setUp(self): + super(HongKongLocaleTests, self).setUp() + + self.locale = locales.HongKongLocale() + + def test_format_timeframe(self): + self.assertEqual(self.locale._format_timeframe("now", 0), "剛才") + self.assertEqual(self.locale._format_timeframe("second", 1), "1秒") + self.assertEqual(self.locale._format_timeframe("seconds", 30), "30秒") + self.assertEqual(self.locale._format_timeframe("minute", 1), "1分鐘") + self.assertEqual(self.locale._format_timeframe("minutes", 40), "40分鐘") + self.assertEqual(self.locale._format_timeframe("hour", 1), "1小時") + self.assertEqual(self.locale._format_timeframe("hours", 23), "23小時") + self.assertEqual(self.locale._format_timeframe("day", 1), "1天") + self.assertEqual(self.locale._format_timeframe("days", 12), "12天") + self.assertEqual(self.locale._format_timeframe("week", 1), "1星期") + self.assertEqual(self.locale._format_timeframe("weeks", 38), "38星期") + self.assertEqual(self.locale._format_timeframe("month", 1), "1個月") + self.assertEqual(self.locale._format_timeframe("months", 11), "11個月") + self.assertEqual(self.locale._format_timeframe("year", 1), "1年") + self.assertEqual(self.locale._format_timeframe("years", 12), "12年") From 760cab284c451480f85b31fbdfd58b76336ed086 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 4 Jan 2020 14:45:18 -0500 Subject: [PATCH 372/649] Moved util tests to pytest --- tests/locales_tests.py | 1 + tests/parser_tests.py.bak | 1629 +++++++++++++++++++++++++++++++++++++ tests/util_tests.py | 27 +- tests/util_tests.py.bak | 40 + 4 files changed, 1683 insertions(+), 14 deletions(-) create mode 100644 tests/parser_tests.py.bak create mode 100644 tests/util_tests.py.bak diff --git a/tests/locales_tests.py b/tests/locales_tests.py index cd1e2e611..1b279ffa7 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -5,6 +5,7 @@ import pytest + class TestLocaleValidation: """Validate locales to ensure that translations are valid and complete""" diff --git a/tests/parser_tests.py.bak b/tests/parser_tests.py.bak new file mode 100644 index 000000000..c72da044b --- /dev/null +++ b/tests/parser_tests.py.bak @@ -0,0 +1,1629 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import calendar +import os +import time +from datetime import datetime + +from chai import Chai +from dateutil import tz + +from arrow import parser +from arrow.constants import MAX_TIMESTAMP_US +from arrow.parser import DateTimeParser, ParserError, ParserMatchError + +from .utils import make_full_tz_list + + +class DateTimeParserTests(Chai): + def setUp(self): + super(DateTimeParserTests, self).setUp() + + self.parser = parser.DateTimeParser() + + def test_parse_multiformat(self): + + mock_datetime = self.mock() + + self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserMatchError) + self.expect(self.parser.parse).args("str", "fmt_b").returns(mock_datetime) + + result = self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) + + self.assertEqual(result, mock_datetime) + + def test_parse_multiformat_all_fail(self): + + self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserMatchError) + self.expect(self.parser.parse).args("str", "fmt_b").raises(ParserMatchError) + + with self.assertRaises(ParserError): + self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) + + def test_parse_multiformat_unself_expected_fail(self): + class UnselfExpectedError(Exception): + pass + + self.expect(self.parser.parse).args("str", "fmt_a").raises(UnselfExpectedError) + + with self.assertRaises(UnselfExpectedError): + self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) + + def test_parse_token_nonsense(self): + parts = {} + self.parser._parse_token("NONSENSE", "1900", parts) + self.assertEqual(parts, {}) + + def test_parse_token_invalid_meridians(self): + parts = {} + self.parser._parse_token("A", "a..m", parts) + self.assertEqual(parts, {}) + self.parser._parse_token("a", "p..m", parts) + self.assertEqual(parts, {}) + + def test_parser_no_caching(self): + + self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( + 100 + ) + self.parser = parser.DateTimeParser(cache_size=0) + for _ in range(100): + self.parser._generate_pattern_re("fmt_a") + + def test_parser_1_line_caching(self): + + self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( + 1 + ) + self.parser = parser.DateTimeParser(cache_size=1) + for _ in range(100): + self.parser._generate_pattern_re("fmt_a") + + self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_b").times( + 1 + ) + for _ in range(100): + self.parser._generate_pattern_re("fmt_a") + self.parser._generate_pattern_re("fmt_b") + + self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( + 1 + ) + for _ in range(100): + self.parser._generate_pattern_re("fmt_a") + + def test_parser_multiple_line_caching(self): + + self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( + 1 + ) + self.parser = parser.DateTimeParser(cache_size=2) + for _ in range(100): + self.parser._generate_pattern_re("fmt_a") + + self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_b").times( + 1 + ) + for _ in range(100): + self.parser._generate_pattern_re("fmt_a") + self.parser._generate_pattern_re("fmt_b") + + self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( + 0 + ) + for _ in range(100): + self.parser._generate_pattern_re("fmt_a") + + def test_YY_and_YYYY_format_list(self): + + self.assertEqual( + self.parser.parse("15/01/19", ["DD/MM/YY", "DD/MM/YYYY"]), + datetime(2019, 1, 15), + ) + + # Regression test for issue #580 + self.assertEqual( + self.parser.parse("15/01/2019", ["DD/MM/YY", "DD/MM/YYYY"]), + datetime(2019, 1, 15), + ) + + self.assertEqual( + 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 + self.assertEqual( + self.parser.parse( + "15 Jul 2000", + ["MM/DD/YYYY", "YYYY-MM-DD", "X", "DD-MMMM-YYYY", "D MMM YYYY"], + ), + datetime(2000, 7, 15), + ) + + with self.assertRaises(ParserError): + self.parser.parse("15 Jul", "X") + + +class DateTimeParserParseTests(Chai): + def setUp(self): + super(DateTimeParserParseTests, self).setUp() + + self.parser = parser.DateTimeParser() + + def test_parse_list(self): + + self.expect(self.parser._parse_multiformat).args( + "str", ["fmt_a", "fmt_b"] + ).returns("result") + + result = self.parser.parse("str", ["fmt_a", "fmt_b"]) + + self.assertEqual(result, "result") + + def test_parse_unrecognized_token(self): + + mock_input_re_map = self.mock(self.parser, "_input_re_map") + + self.expect(mock_input_re_map.__getitem__).args("YYYY").raises(KeyError) + + with self.assertRaises(parser.ParserError): + self.parser.parse("2013-01-01", "YYYY-MM-DD") + + def test_parse_parse_no_match(self): + + with self.assertRaises(ParserError): + self.parser.parse("01-01", "YYYY-MM-DD") + + def test_parse_separators(self): + + with self.assertRaises(ParserError): + self.parser.parse("1403549231", "YYYY-MM-DD") + + def test_parse_numbers(self): + + self.expected = datetime(2012, 1, 1, 12, 5, 10) + self.assertEqual( + self.parser.parse("2012-01-01 12:05:10", "YYYY-MM-DD HH:mm:ss"), + self.expected, + ) + + def test_parse_year_two_digit(self): + + self.expected = datetime(1979, 1, 1, 12, 5, 10) + self.assertEqual( + 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() + int_timestamp = int(time.time()) + self.expected = datetime.fromtimestamp(int_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:d}".format(int_timestamp), "X"), self.expected + ) + + float_timestamp = time.time() + self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:f}".format(float_timestamp), "X"), self.expected + ) + + # test handling of ns timestamp (arrow will round to 6 digits regardless) + self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:f}123".format(float_timestamp), "X"), self.expected + ) + + # test ps timestamp (arrow will round to 6 digits regardless) + self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:f}123456".format(float_timestamp), "X"), self.expected + ) + + # NOTE: negative timestamps cannot be handled by datetime on Window + # Must use timedelta to handle them. ref: https://stackoverflow.com/questions/36179914 + if os.name != "nt": + # regression test for issue #662 + negative_int_timestamp = -int_timestamp + self.expected = datetime.fromtimestamp(negative_int_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:d}".format(negative_int_timestamp), "X"), + self.expected, + ) + + negative_float_timestamp = -float_timestamp + self.expected = datetime.fromtimestamp(negative_float_timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:f}".format(negative_float_timestamp), "X"), + self.expected, + ) + + # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will + # break cases like "15 Jul 2000" and a format list (see issue #447) + with self.assertRaises(ParserError): + natural_lang_string = "Meet me at {} at the restaurant.".format( + float_timestamp + ) + self.parser.parse(natural_lang_string, "X") + + with self.assertRaises(ParserError): + self.parser.parse("1565982019.", "X") + + with self.assertRaises(ParserError): + self.parser.parse(".1565982019", "X") + + def test_parse_expanded_timestamp(self): + # test expanded timestamps that include milliseconds + # and microseconds as multiples rather than decimals + # requested in issue #357 + + tz_utc = tz.tzutc() + timestamp = 1569982581.413132 + timestamp_milli = int(round(timestamp * 1000)) + timestamp_micro = int(round(timestamp * 1000000)) + + # "x" token should parse integer timestamps below MAX_TIMESTAMP normally + self.expected = datetime.fromtimestamp(int(timestamp), tz=tz_utc) + self.assertEqual( + self.parser.parse("{:d}".format(int(timestamp)), "x"), self.expected + ) + + self.expected = datetime.fromtimestamp(round(timestamp, 3), tz=tz_utc) + self.assertEqual( + self.parser.parse("{:d}".format(timestamp_milli), "x"), self.expected + ) + + self.expected = datetime.fromtimestamp(timestamp, tz=tz_utc) + self.assertEqual( + self.parser.parse("{:d}".format(timestamp_micro), "x"), self.expected + ) + + # anything above max µs timestamp should fail + with self.assertRaises(ValueError): + self.parser.parse("{:d}".format(int(MAX_TIMESTAMP_US) + 1), "x") + + # floats are not allowed with the "x" token + with self.assertRaises(ParserMatchError): + self.parser.parse("{:f}".format(timestamp), "x") + + def test_parse_names(self): + + self.expected = datetime(2012, 1, 1) + + self.assertEqual( + self.parser.parse("January 1, 2012", "MMMM D, YYYY"), self.expected + ) + self.assertEqual(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) + self.assertEqual(self.parser.parse("1 pm", "H a"), self.expected) + self.assertEqual(self.parser.parse("1 pm", "h a"), self.expected) + + self.expected = datetime(1, 1, 1, 1, 0, 0) + self.assertEqual(self.parser.parse("1 am", "H A"), self.expected) + self.assertEqual(self.parser.parse("1 am", "h A"), self.expected) + + self.expected = datetime(1, 1, 1, 0, 0, 0) + self.assertEqual(self.parser.parse("12 am", "H A"), self.expected) + self.assertEqual(self.parser.parse("12 am", "h A"), self.expected) + + self.expected = datetime(1, 1, 1, 12, 0, 0) + self.assertEqual(self.parser.parse("12 pm", "H A"), self.expected) + self.assertEqual(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") + self.assertEqual(parsed, self.expected) + + def test_parse_tz_zz(self): + + self.expected = datetime(2013, 1, 1, tzinfo=tz.tzoffset(None, -7 * 3600)) + self.assertEqual( + self.parser.parse("2013-01-01 -07:00", "YYYY-MM-DD ZZ"), self.expected + ) + + def test_parse_tz_name_zzz(self): + for tz_name in make_full_tz_list(): + self.expected = datetime(2013, 1, 1, tzinfo=tz.gettz(tz_name)) + self.assertEqual( + self.parser.parse("2013-01-01 %s" % tz_name, "YYYY-MM-DD ZZZ"), + self.expected, + ) + + # note that offsets are not timezones + with self.assertRaises(ParserError): + self.parser.parse("2013-01-01 12:30:45.9+1000", "YYYY-MM-DDZZZ") + + with self.assertRaises(ParserError): + self.parser.parse("2013-01-01 12:30:45.9+10:00", "YYYY-MM-DDZZZ") + + with self.assertRaises(ParserError): + self.parser.parse("2013-01-01 12:30:45.9-10", "YYYY-MM-DDZZZ") + + def test_parse_subsecond(self): + # TODO: make both test_parse_subsecond functions in Parse and ParseISO + # tests use the same expected objects (use pytest fixtures) + self.expected = datetime(2013, 1, 1, 12, 30, 45, 900000) + self.assertEqual( + self.parser.parse("2013-01-01 12:30:45.9", "YYYY-MM-DD HH:mm:ss.S"), + self.expected, + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 980000) + self.assertEqual( + self.parser.parse("2013-01-01 12:30:45.98", "YYYY-MM-DD HH:mm:ss.SS"), + self.expected, + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987000) + self.assertEqual( + self.parser.parse("2013-01-01 12:30:45.987", "YYYY-MM-DD HH:mm:ss.SSS"), + self.expected, + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987600) + self.assertEqual( + self.parser.parse("2013-01-01 12:30:45.9876", "YYYY-MM-DD HH:mm:ss.SSSS"), + self.expected, + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987650) + self.assertEqual( + self.parser.parse("2013-01-01 12:30:45.98765", "YYYY-MM-DD HH:mm:ss.SSSSS"), + self.expected, + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) + self.assertEqual( + self.parser.parse( + "2013-01-01 12:30:45.987654", "YYYY-MM-DD HH:mm:ss.SSSSSS" + ), + self.expected, + ) + + def test_parse_subsecond_rounding(self): + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) + datetime_format = "YYYY-MM-DD HH:mm:ss.S" + + # round up + string = "2013-01-01 12:30:45.9876539" + self.assertEqual(self.parser.parse(string, datetime_format), self.expected) + self.assertEqual(self.parser.parse_iso(string), self.expected) + + # round down + string = "2013-01-01 12:30:45.98765432" + self.assertEqual(self.parser.parse(string, datetime_format), self.expected) + self.assertEqual(self.parser.parse_iso(string), self.expected) + + # round half-up + string = "2013-01-01 12:30:45.987653521" + self.assertEqual(self.parser.parse(string, datetime_format), self.expected) + self.assertEqual(self.parser.parse_iso(string), self.expected) + + # round half-down + string = "2013-01-01 12:30:45.9876545210" + self.assertEqual(self.parser.parse(string, datetime_format), self.expected) + self.assertEqual(self.parser.parse_iso(string), self.expected) + + # overflow (zero out the subseconds and increment the seconds) + # regression tests for issue #636 + def test_parse_subsecond_rounding_overflow(self): + datetime_format = "YYYY-MM-DD HH:mm:ss.S" + + self.expected = datetime(2013, 1, 1, 12, 30, 46) + string = "2013-01-01 12:30:45.9999995" + self.assertEqual(self.parser.parse(string, datetime_format), self.expected) + self.assertEqual(self.parser.parse_iso(string), self.expected) + + self.expected = datetime(2013, 1, 1, 12, 31, 0) + string = "2013-01-01 12:30:59.9999999" + self.assertEqual(self.parser.parse(string, datetime_format), self.expected) + self.assertEqual(self.parser.parse_iso(string), self.expected) + + self.expected = datetime(2013, 1, 2, 0, 0, 0) + string = "2013-01-01 23:59:59.9999999" + self.assertEqual(self.parser.parse(string, datetime_format), self.expected) + self.assertEqual(self.parser.parse_iso(string), self.expected) + + # 6 digits should remain unrounded + self.expected = datetime(2013, 1, 1, 12, 30, 45, 999999) + string = "2013-01-01 12:30:45.999999" + self.assertEqual(self.parser.parse(string, datetime_format), self.expected) + self.assertEqual(self.parser.parse_iso(string), self.expected) + + # Regression tests for issue #560 + def test_parse_long_year(self): + with self.assertRaises(ParserError): + self.parser.parse("09 January 123456789101112", "DD MMMM YYYY") + + with self.assertRaises(ParserError): + self.parser.parse("123456789101112 09 January", "YYYY DD MMMM") + + with self.assertRaises(ParserError): + self.parser.parse("68096653015/01/19", "YY/M/DD") + + def test_parse_with_extra_words_at_start_and_end_invalid(self): + input_format_pairs = [ + ("blah2016", "YYYY"), + ("blah2016blah", "YYYY"), + ("2016blah", "YYYY"), + ("2016-05blah", "YYYY-MM"), + ("2016-05-16blah", "YYYY-MM-DD"), + ("2016-05-16T04:05:06.789120blah", "YYYY-MM-DDThh:mm:ss.S"), + ("2016-05-16T04:05:06.789120ZblahZ", "YYYY-MM-DDThh:mm:ss.SZ"), + ("2016-05-16T04:05:06.789120Zblah", "YYYY-MM-DDThh:mm:ss.SZ"), + ("2016-05-16T04:05:06.789120blahZ", "YYYY-MM-DDThh:mm:ss.SZ"), + ] + + for pair in input_format_pairs: + with self.assertRaises(ParserError): + self.parser.parse(pair[0], pair[1]) + + def test_parse_with_extra_words_at_start_and_end_valid(self): + # Spaces surrounding the parsable date are ok because we + # allow the parsing of natural language input. Additionally, a single + # character of specific punctuation before or after the date is okay. + # See docs for full list of valid punctuation. + + self.assertEqual( + self.parser.parse("blah 2016 blah", "YYYY"), datetime(2016, 1, 1) + ) + + self.assertEqual(self.parser.parse("blah 2016", "YYYY"), datetime(2016, 1, 1)) + + self.assertEqual(self.parser.parse("2016 blah", "YYYY"), datetime(2016, 1, 1)) + + # test one additional space along with space divider + self.assertEqual( + self.parser.parse( + "blah 2016-05-16 04:05:06.789120", "YYYY-MM-DD hh:mm:ss.S" + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + self.assertEqual( + self.parser.parse( + "2016-05-16 04:05:06.789120 blah", "YYYY-MM-DD hh:mm:ss.S" + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + # test one additional space along with T divider + self.assertEqual( + self.parser.parse( + "blah 2016-05-16T04:05:06.789120", "YYYY-MM-DDThh:mm:ss.S" + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + self.assertEqual( + self.parser.parse( + "2016-05-16T04:05:06.789120 blah", "YYYY-MM-DDThh:mm:ss.S" + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + self.assertEqual( + 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), + ) + + self.assertEqual( + 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 + # for the list of valid punctuation, see documentation + def test_parse_with_punctuation_fences(self): + self.assertEqual( + self.parser.parse( + "Meet me at my house on Halloween (2019-31-10)", "YYYY-DD-MM" + ), + datetime(2019, 10, 31), + ) + + self.assertEqual( + self.parser.parse( + "Monday, 9. September 2019, 16:15-20:00", "dddd, D. MMMM YYYY" + ), + datetime(2019, 9, 9), + ) + + self.assertEqual( + self.parser.parse("A date is 11.11.2011.", "DD.MM.YYYY"), + datetime(2011, 11, 11), + ) + + with self.assertRaises(ParserMatchError): + self.parser.parse("11.11.2011.1 is not a valid date.", "DD.MM.YYYY") + + with self.assertRaises(ParserMatchError): + self.parser.parse( + "This date has too many punctuation marks following it (11.11.2011).", + "DD.MM.YYYY", + ) + + def test_parse_with_leading_and_trailing_whitespace(self): + self.assertEqual(self.parser.parse(" 2016", "YYYY"), datetime(2016, 1, 1)) + + self.assertEqual(self.parser.parse("2016 ", "YYYY"), datetime(2016, 1, 1)) + + self.assertEqual( + self.parser.parse(" 2016 ", "YYYY"), datetime(2016, 1, 1) + ) + + self.assertEqual( + self.parser.parse( + " 2016-05-16 04:05:06.789120 ", "YYYY-MM-DD hh:mm:ss.S" + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + self.assertEqual( + self.parser.parse( + " 2016-05-16T04:05:06.789120 ", "YYYY-MM-DDThh:mm:ss.S" + ), + datetime(2016, 5, 16, 4, 5, 6, 789120), + ) + + def test_parse_YYYY_DDDD(self): + self.assertEqual( + self.parser.parse("1998-136", "YYYY-DDDD"), datetime(1998, 5, 16) + ) + + self.assertEqual( + self.parser.parse("1998-006", "YYYY-DDDD"), datetime(1998, 1, 6) + ) + + with self.assertRaises(ParserError): + self.parser.parse("1998-456", "YYYY-DDDD") + + def test_parse_YYYY_DDD(self): + self.assertEqual(self.parser.parse("1998-6", "YYYY-DDD"), datetime(1998, 1, 6)) + + self.assertEqual( + self.parser.parse("1998-136", "YYYY-DDD"), datetime(1998, 5, 16) + ) + + with self.assertRaises(ParserError): + self.parser.parse("1998-756", "YYYY-DDD") + + # month cannot be passed with DDD and DDDD tokens + def test_parse_YYYY_MM_DDDD(self): + with self.assertRaises(ParserError): + self.parser.parse("2015-01-009", "YYYY-MM-DDDD") + + # year is required with the DDD and DDDD tokens + def test_parse_DDD_only(self): + with self.assertRaises(ParserError): + self.parser.parse("5", "DDD") + + def test_parse_DDDD_only(self): + with self.assertRaises(ParserError): + self.parser.parse("145", "DDDD") + + def test_parse_HH_24(self): + self.assertEqual( + self.parser.parse("2019-10-30T24:00:00", "YYYY-MM-DDTHH:mm:ss"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-10-30T24:00", "YYYY-MM-DDTHH:mm"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-10-30T24", "YYYY-MM-DDTHH"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-10-30T24:00:00.0", "YYYY-MM-DDTHH:mm:ss.S"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-10-31T24:00:00", "YYYY-MM-DDTHH:mm:ss"), + datetime(2019, 11, 1, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-12-31T24:00:00", "YYYY-MM-DDTHH:mm:ss"), + datetime(2020, 1, 1, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse("2019-12-31T23:59:59.9999999", "YYYY-MM-DDTHH:mm:ss.S"), + datetime(2020, 1, 1, 0, 0, 0, 0), + ) + + with self.assertRaises(ParserError): + self.parser.parse("2019-12-31T24:01:00", "YYYY-MM-DDTHH:mm:ss") + + with self.assertRaises(ParserError): + self.parser.parse("2019-12-31T24:00:01", "YYYY-MM-DDTHH:mm:ss") + + with self.assertRaises(ParserError): + self.parser.parse("2019-12-31T24:00:00.1", "YYYY-MM-DDTHH:mm:ss.S") + + with self.assertRaises(ParserError): + self.parser.parse("2019-12-31T24:00:00.999999", "YYYY-MM-DDTHH:mm:ss.S") + + +class DateTimeParserRegexTests(Chai): + def setUp(self): + super(DateTimeParserRegexTests, self).setUp() + + self.format_regex = parser.DateTimeParser._FORMAT_RE + + def test_format_year(self): + + self.assertEqual(self.format_regex.findall("YYYY-YY"), ["YYYY", "YY"]) + + def test_format_month(self): + + self.assertEqual( + self.format_regex.findall("MMMM-MMM-MM-M"), ["MMMM", "MMM", "MM", "M"] + ) + + def test_format_day(self): + + self.assertEqual( + self.format_regex.findall("DDDD-DDD-DD-D"), ["DDDD", "DDD", "DD", "D"] + ) + + def test_format_hour(self): + + self.assertEqual(self.format_regex.findall("HH-H-hh-h"), ["HH", "H", "hh", "h"]) + + def test_format_minute(self): + + self.assertEqual(self.format_regex.findall("mm-m"), ["mm", "m"]) + + def test_format_second(self): + + self.assertEqual(self.format_regex.findall("ss-s"), ["ss", "s"]) + + def test_format_subsecond(self): + + self.assertEqual( + self.format_regex.findall("SSSSSS-SSSSS-SSSS-SSS-SS-S"), + ["SSSSSS", "SSSSS", "SSSS", "SSS", "SS", "S"], + ) + + def test_format_tz(self): + + self.assertEqual(self.format_regex.findall("ZZZ-ZZ-Z"), ["ZZZ", "ZZ", "Z"]) + + def test_format_am_pm(self): + + self.assertEqual(self.format_regex.findall("A-a"), ["A", "a"]) + + def test_format_timestamp(self): + + self.assertEqual(self.format_regex.findall("X"), ["X"]) + + def test_format_timestamp_milli(self): + + self.assertEqual(self.format_regex.findall("x"), ["x"]) + + def test_escape(self): + + escape_regex = parser.DateTimeParser._ESCAPE_RE + + self.assertEqual( + escape_regex.findall("2018-03-09 8 [h] 40 [hello]"), ["[h]", "[hello]"] + ) + + def test_month_names(self): + p = parser.DateTimeParser("en_us") + + text = "_".join(calendar.month_name[1:]) + + result = p._input_re_map["MMMM"].findall(text) + + self.assertEqual(result, calendar.month_name[1:]) + + def test_month_abbreviations(self): + p = parser.DateTimeParser("en_us") + + text = "_".join(calendar.month_abbr[1:]) + + result = p._input_re_map["MMM"].findall(text) + + self.assertEqual(result, calendar.month_abbr[1:]) + + def test_digits(self): + + self.assertEqual( + parser.DateTimeParser._ONE_OR_TWO_DIGIT_RE.findall("4-56"), ["4", "56"] + ) + self.assertEqual( + parser.DateTimeParser._ONE_OR_TWO_OR_THREE_DIGIT_RE.findall("4-56-789"), + ["4", "56", "789"], + ) + self.assertEqual( + parser.DateTimeParser._ONE_OR_MORE_DIGIT_RE.findall("4-56-789-1234-12345"), + ["4", "56", "789", "1234", "12345"], + ) + self.assertEqual( + parser.DateTimeParser._TWO_DIGIT_RE.findall("12-3-45"), ["12", "45"] + ) + self.assertEqual( + parser.DateTimeParser._THREE_DIGIT_RE.findall("123-4-56"), ["123"] + ) + self.assertEqual( + parser.DateTimeParser._FOUR_DIGIT_RE.findall("1234-56"), ["1234"] + ) + + def test_tz(self): + tz_z_re = parser.DateTimeParser._TZ_Z_RE + self.assertEqual(tz_z_re.findall("-0700"), [("-", "07", "00")]) + self.assertEqual(tz_z_re.findall("+07"), [("+", "07", "")]) + self.assertTrue(tz_z_re.search("15/01/2019T04:05:06.789120Z") is not None) + self.assertTrue(tz_z_re.search("15/01/2019T04:05:06.789120") is None) + + tz_zz_re = parser.DateTimeParser._TZ_ZZ_RE + self.assertEqual(tz_zz_re.findall("-07:00"), [("-", "07", "00")]) + self.assertEqual(tz_zz_re.findall("+07"), [("+", "07", "")]) + self.assertTrue(tz_zz_re.search("15/01/2019T04:05:06.789120Z") is not None) + self.assertTrue(tz_zz_re.search("15/01/2019T04:05:06.789120") is None) + + tz_name_re = parser.DateTimeParser._TZ_NAME_RE + self.assertEqual(tz_name_re.findall("Europe/Warsaw"), ["Europe/Warsaw"]) + self.assertEqual(tz_name_re.findall("GMT"), ["GMT"]) + + def test_timestamp(self): + timestamp_re = parser.DateTimeParser._TIMESTAMP_RE + self.assertEqual( + timestamp_re.findall("1565707550.452729"), ["1565707550.452729"] + ) + self.assertEqual( + timestamp_re.findall("-1565707550.452729"), ["-1565707550.452729"] + ) + self.assertEqual(timestamp_re.findall("-1565707550"), ["-1565707550"]) + self.assertEqual(timestamp_re.findall("1565707550"), ["1565707550"]) + self.assertEqual(timestamp_re.findall("1565707550."), []) + self.assertEqual(timestamp_re.findall(".1565707550"), []) + + def test_timestamp_milli(self): + timestamp_expanded_re = parser.DateTimeParser._TIMESTAMP_EXPANDED_RE + self.assertEqual(timestamp_expanded_re.findall("-1565707550"), ["-1565707550"]) + self.assertEqual(timestamp_expanded_re.findall("1565707550"), ["1565707550"]) + self.assertEqual(timestamp_expanded_re.findall("1565707550.452729"), []) + self.assertEqual(timestamp_expanded_re.findall("1565707550."), []) + self.assertEqual(timestamp_expanded_re.findall(".1565707550"), []) + + def test_time(self): + time_re = parser.DateTimeParser._TIME_RE + time_seperators = [":", ""] + + for sep in time_seperators: + self.assertEqual(time_re.findall("12"), [("12", "", "", "", "")]) + self.assertEqual( + time_re.findall("12{sep}35".format(sep=sep)), [("12", "35", "", "", "")] + ) + self.assertEqual( + time_re.findall("12{sep}35{sep}46".format(sep=sep)), + [("12", "35", "46", "", "")], + ) + self.assertEqual( + time_re.findall("12{sep}35{sep}46.952313".format(sep=sep)), + [("12", "35", "46", ".", "952313")], + ) + self.assertEqual( + time_re.findall("12{sep}35{sep}46,952313".format(sep=sep)), + [("12", "35", "46", ",", "952313")], + ) + + self.assertEqual(time_re.findall("12:"), []) + self.assertEqual(time_re.findall("12:35:46."), []) + self.assertEqual(time_re.findall("12:35:46,"), []) + + +class DateTimeParserISOTests(Chai): + def setUp(self): + super(DateTimeParserISOTests, self).setUp() + + self.parser = parser.DateTimeParser("en_us") + + def test_YYYY(self): + + self.assertEqual(self.parser.parse_iso("2013"), datetime(2013, 1, 1)) + + def test_YYYY_DDDD(self): + self.assertEqual(self.parser.parse_iso("1998-136"), datetime(1998, 5, 16)) + + self.assertEqual(self.parser.parse_iso("1998-006"), datetime(1998, 1, 6)) + + with self.assertRaises(ParserError): + self.parser.parse_iso("1998-456") + + # 2016 is a leap year, so Feb 29 exists (leap day) + self.assertEqual(self.parser.parse_iso("2016-059"), datetime(2016, 2, 28)) + self.assertEqual(self.parser.parse_iso("2016-060"), datetime(2016, 2, 29)) + self.assertEqual(self.parser.parse_iso("2016-061"), datetime(2016, 3, 1)) + + # 2017 is not a leap year, so Feb 29 does not exist + self.assertEqual(self.parser.parse_iso("2017-059"), datetime(2017, 2, 28)) + self.assertEqual(self.parser.parse_iso("2017-060"), datetime(2017, 3, 1)) + self.assertEqual(self.parser.parse_iso("2017-061"), datetime(2017, 3, 2)) + + # Since 2016 is a leap year, the 366th day falls in the same year + self.assertEqual(self.parser.parse_iso("2016-366"), datetime(2016, 12, 31)) + + # Since 2017 is not a leap year, the 366th day falls in the next year + self.assertEqual(self.parser.parse_iso("2017-366"), datetime(2018, 1, 1)) + + def test_YYYY_DDDD_HH_mm_ssZ(self): + + self.assertEqual( + self.parser.parse_iso("2013-036 04:05:06+01:00"), + datetime(2013, 2, 5, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600)), + ) + + self.assertEqual( + self.parser.parse_iso("2013-036 04:05:06Z"), + datetime(2013, 2, 5, 4, 5, 6, tzinfo=tz.tzutc()), + ) + + def test_YYYY_MM_DDDD(self): + with self.assertRaises(ParserError): + self.parser.parse_iso("2014-05-125") + + def test_YYYY_MM(self): + + for separator in DateTimeParser.SEPARATORS: + self.assertEqual( + self.parser.parse_iso(separator.join(("2013", "02"))), + datetime(2013, 2, 1), + ) + + def test_YYYY_MM_DD(self): + + for separator in DateTimeParser.SEPARATORS: + self.assertEqual( + self.parser.parse_iso(separator.join(("2013", "02", "03"))), + datetime(2013, 2, 3), + ) + + def test_YYYY_MM_DDTHH_mmZ(self): + + self.assertEqual( + 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): + + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05"), datetime(2013, 2, 3, 4, 5) + ) + + def test_YYYY_MM_DDTHH(self): + + self.assertEqual( + self.parser.parse_iso("2013-02-03T04"), datetime(2013, 2, 3, 4) + ) + + def test_YYYY_MM_DDTHHZ(self): + + self.assertEqual( + 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): + + self.assertEqual( + 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): + + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06"), datetime(2013, 2, 3, 4, 5, 6) + ) + + def test_YYYY_MM_DD_HH_mmZ(self): + + self.assertEqual( + 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): + + self.assertEqual( + self.parser.parse_iso("2013-02-03 04:05"), datetime(2013, 2, 3, 4, 5) + ) + + def test_YYYY_MM_DD_HH(self): + + self.assertEqual( + self.parser.parse_iso("2013-02-03 04"), datetime(2013, 2, 3, 4) + ) + + def test_invalid_time(self): + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03 044") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03 04:05:06.") + + def test_YYYY_MM_DD_HH_mm_ssZ(self): + + self.assertEqual( + 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): + + self.assertEqual( + 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): + + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.7"), + datetime(2013, 2, 3, 4, 5, 6, 700000), + ) + + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.78"), + datetime(2013, 2, 3, 4, 5, 6, 780000), + ) + + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.789"), + datetime(2013, 2, 3, 4, 5, 6, 789000), + ) + + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.7891"), + datetime(2013, 2, 3, 4, 5, 6, 789100), + ) + + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.78912"), + datetime(2013, 2, 3, 4, 5, 6, 789120), + ) + + # ISO 8601:2004(E), ISO, 2004-12-01, 4.2.2.4 ... the decimal fraction + # shall be divided from the integer part by the decimal sign specified + # in ISO 31-0, i.e. the comma [,] or full stop [.]. Of these, the comma + # is the preferred sign. + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06,789123678"), + datetime(2013, 2, 3, 4, 5, 6, 789124), + ) + + # there is no limit on the number of decimal places + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.789123678"), + datetime(2013, 2, 3, 4, 5, 6, 789124), + ) + + def test_YYYY_MM_DDTHH_mm_ss_SZ(self): + + self.assertEqual( + 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)), + ) + + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.78+01:00"), + datetime(2013, 2, 3, 4, 5, 6, 780000, tzinfo=tz.tzoffset(None, 3600)), + ) + + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.789+01:00"), + datetime(2013, 2, 3, 4, 5, 6, 789000, tzinfo=tz.tzoffset(None, 3600)), + ) + + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.7891+01:00"), + datetime(2013, 2, 3, 4, 5, 6, 789100, tzinfo=tz.tzoffset(None, 3600)), + ) + + self.assertEqual( + self.parser.parse_iso("2013-02-03T04:05:06.78912+01:00"), + datetime(2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzoffset(None, 3600)), + ) + + self.assertEqual( + self.parser.parse_iso("2013-02-03 04:05:06.78912Z"), + datetime(2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzutc()), + ) + + def test_invalid_Z(self): + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912z") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912zz") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912Zz") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912ZZ") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912+Z") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912-Z") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912 Z") + + def test_parse_subsecond(self): + # TODO: make both test_parse_subsecond functions in Parse and ParseISO + # tests use the same expected objects (use pytest fixtures) + self.expected = datetime(2013, 1, 1, 12, 30, 45, 900000) + self.assertEqual(self.parser.parse_iso("2013-01-01 12:30:45.9"), self.expected) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 980000) + self.assertEqual(self.parser.parse_iso("2013-01-01 12:30:45.98"), self.expected) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987000) + self.assertEqual( + self.parser.parse_iso("2013-01-01 12:30:45.987"), self.expected + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987600) + self.assertEqual( + self.parser.parse_iso("2013-01-01 12:30:45.9876"), self.expected + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987650) + self.assertEqual( + self.parser.parse_iso("2013-01-01 12:30:45.98765"), self.expected + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) + self.assertEqual( + self.parser.parse_iso("2013-01-01 12:30:45.987654"), self.expected + ) + + # use comma as subsecond separator + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) + self.assertEqual( + self.parser.parse_iso("2013-01-01 12:30:45,987654"), self.expected + ) + + def test_gnu_date(self): + """ + regression tests for parsing output from GNU date(1) + """ + # date -Ins + self.assertEqual( + self.parser.parse_iso("2016-11-16T09:46:30,895636557-0800"), + datetime( + 2016, 11, 16, 9, 46, 30, 895636, tzinfo=tz.tzoffset(None, -3600 * 8) + ), + ) + + # date --rfc-3339=ns + self.assertEqual( + self.parser.parse_iso("2016-11-16 09:51:14.682141526-08:00"), + datetime( + 2016, 11, 16, 9, 51, 14, 682142, tzinfo=tz.tzoffset(None, -3600 * 8) + ), + ) + + def test_isoformat(self): + + dt = datetime.utcnow() + + self.assertEqual(self.parser.parse_iso(dt.isoformat()), dt) + + def test_parse_iso_with_leading_and_trailing_whitespace(self): + datetime_string = " 2016-11-15T06:37:19.123456" + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + + datetime_string = " 2016-11-15T06:37:19.123456 " + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + + datetime_string = "2016-11-15T06:37:19.123456 " + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + + datetime_string = "2016-11-15T 06:37:19.123456" + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + + # leading whitespace + datetime_string = " 2016-11-15 06:37:19.123456" + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + + # trailing whitespace + datetime_string = "2016-11-15 06:37:19.123456 " + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + + datetime_string = " 2016-11-15 06:37:19.123456 " + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + + # two dividing spaces + datetime_string = "2016-11-15 06:37:19.123456" + with self.assertRaises(ParserError): + self.parser.parse_iso(datetime_string) + + def test_parse_iso_with_extra_words_at_start_and_end_invalid(self): + test_inputs = [ + "blah2016", + "blah2016blah", + "blah 2016 blah", + "blah 2016", + "2016 blah", + "blah 2016-05-16 04:05:06.789120", + "2016-05-16 04:05:06.789120 blah", + "blah 2016-05-16T04:05:06.789120", + "2016-05-16T04:05:06.789120 blah", + "2016blah", + "2016-05blah", + "2016-05-16blah", + "2016-05-16T04:05:06.789120blah", + "2016-05-16T04:05:06.789120ZblahZ", + "2016-05-16T04:05:06.789120Zblah", + "2016-05-16T04:05:06.789120blahZ", + "Meet me at 2016-05-16T04:05:06.789120 at the restaurant.", + "Meet me at 2016-05-16 04:05:06.789120 at the restaurant.", + ] + + for ti in test_inputs: + with self.assertRaises(ParserError): + self.parser.parse_iso(ti) + + def test_iso8601_basic_format(self): + self.assertEqual(self.parser.parse_iso("20180517"), datetime(2018, 5, 17)) + + self.assertEqual( + self.parser.parse_iso("20180517T10"), datetime(2018, 5, 17, 10) + ) + + self.assertEqual( + self.parser.parse_iso("20180517T105513.843456"), + datetime(2018, 5, 17, 10, 55, 13, 843456), + ) + + self.assertEqual( + self.parser.parse_iso("20180517T105513Z"), + datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzutc()), + ) + + self.assertEqual( + self.parser.parse_iso("20180517T105513.843456-0700"), + datetime(2018, 5, 17, 10, 55, 13, 843456, tzinfo=tz.tzoffset(None, -25200)), + ) + + self.assertEqual( + self.parser.parse_iso("20180517T105513-0700"), + datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), + ) + + self.assertEqual( + self.parser.parse_iso("20180517T105513-07"), + datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), + ) + + # ordinal in basic format: YYYYDDDD + self.assertEqual(self.parser.parse_iso("1998136"), datetime(1998, 5, 16)) + + # timezone requires +- seperator + with self.assertRaises(ParserError): + self.parser.parse_iso("20180517T1055130700") + + with self.assertRaises(ParserError): + self.parser.parse_iso("20180517T10551307") + + # too many digits in date + with self.assertRaises(ParserError): + self.parser.parse_iso("201860517T105513Z") + + # too many digits in time + with self.assertRaises(ParserError): + self.parser.parse_iso("20180517T1055213Z") + + def test_midnight_end_day(self): + self.assertEqual( + self.parser.parse_iso("2019-10-30T24:00:00"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse_iso("2019-10-30T24:00"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse_iso("2019-10-30T24:00:00.0"), + datetime(2019, 10, 31, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse_iso("2019-10-31T24:00:00"), + datetime(2019, 11, 1, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse_iso("2019-12-31T24:00:00"), + datetime(2020, 1, 1, 0, 0, 0, 0), + ) + self.assertEqual( + self.parser.parse_iso("2019-12-31T23:59:59.9999999"), + datetime(2020, 1, 1, 0, 0, 0, 0), + ) + + with self.assertRaises(ParserError): + self.parser.parse_iso("2019-12-31T24:01:00") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2019-12-31T24:00:01") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2019-12-31T24:00:00.1") + + with self.assertRaises(ParserError): + self.parser.parse_iso("2019-12-31T24:00:00.999999") + + +class TzinfoParserTests(Chai): + def setUp(self): + super(TzinfoParserTests, self).setUp() + + self.parser = parser.TzinfoParser() + + def test_parse_local(self): + + self.assertEqual(self.parser.parse("local"), tz.tzlocal()) + + def test_parse_utc(self): + + self.assertEqual(self.parser.parse("utc"), tz.tzutc()) + self.assertEqual(self.parser.parse("UTC"), tz.tzutc()) + + def test_parse_iso(self): + + self.assertEqual(self.parser.parse("01:00"), tz.tzoffset(None, 3600)) + self.assertEqual( + self.parser.parse("11:35"), tz.tzoffset(None, 11 * 3600 + 2100) + ) + self.assertEqual(self.parser.parse("+01:00"), tz.tzoffset(None, 3600)) + self.assertEqual(self.parser.parse("-01:00"), tz.tzoffset(None, -3600)) + + self.assertEqual(self.parser.parse("0100"), tz.tzoffset(None, 3600)) + self.assertEqual(self.parser.parse("+0100"), tz.tzoffset(None, 3600)) + self.assertEqual(self.parser.parse("-0100"), tz.tzoffset(None, -3600)) + + self.assertEqual(self.parser.parse("01"), tz.tzoffset(None, 3600)) + self.assertEqual(self.parser.parse("+01"), tz.tzoffset(None, 3600)) + self.assertEqual(self.parser.parse("-01"), tz.tzoffset(None, -3600)) + + def test_parse_str(self): + + self.assertEqual(self.parser.parse("US/Pacific"), tz.gettz("US/Pacific")) + + def test_parse_fails(self): + + with self.assertRaises(parser.ParserError): + self.parser.parse("fail") + + +class DateTimeParserMonthNameTests(Chai): + def setUp(self): + super(DateTimeParserMonthNameTests, self).setUp() + + self.parser = parser.DateTimeParser("en_us") + + def test_shortmonth_capitalized(self): + + self.assertEqual( + self.parser.parse("2013-Jan-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) + ) + + def test_shortmonth_allupper(self): + + self.assertEqual( + self.parser.parse("2013-JAN-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) + ) + + def test_shortmonth_alllower(self): + + self.assertEqual( + self.parser.parse("2013-jan-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) + ) + + def test_month_capitalized(self): + + self.assertEqual( + self.parser.parse("2013-January-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) + ) + + def test_month_allupper(self): + + self.assertEqual( + self.parser.parse("2013-JANUARY-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) + ) + + def test_month_alllower(self): + + self.assertEqual( + self.parser.parse("2013-january-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) + ) + + def test_localized_month_name(self): + parser_ = parser.DateTimeParser("fr_fr") + + self.assertEqual( + parser_.parse("2013-Janvier-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) + ) + + def test_localized_month_abbreviation(self): + parser_ = parser.DateTimeParser("it_it") + + self.assertEqual( + parser_.parse("2013-Gen-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) + ) + + +class DateTimeParserMeridiansTests(Chai): + def setUp(self): + super(DateTimeParserMeridiansTests, self).setUp() + + self.parser = parser.DateTimeParser("en_us") + + def test_meridians_lowercase(self): + self.assertEqual( + self.parser.parse("2013-01-01 5am", "YYYY-MM-DD ha"), + datetime(2013, 1, 1, 5), + ) + + self.assertEqual( + self.parser.parse("2013-01-01 5pm", "YYYY-MM-DD ha"), + datetime(2013, 1, 1, 17), + ) + + def test_meridians_capitalized(self): + self.assertEqual( + self.parser.parse("2013-01-01 5AM", "YYYY-MM-DD hA"), + datetime(2013, 1, 1, 5), + ) + + self.assertEqual( + self.parser.parse("2013-01-01 5PM", "YYYY-MM-DD hA"), + datetime(2013, 1, 1, 17), + ) + + def test_localized_meridians_lowercase(self): + parser_ = parser.DateTimeParser("hu_hu") + self.assertEqual( + parser_.parse("2013-01-01 5 de", "YYYY-MM-DD h a"), datetime(2013, 1, 1, 5) + ) + + self.assertEqual( + parser_.parse("2013-01-01 5 du", "YYYY-MM-DD h a"), datetime(2013, 1, 1, 17) + ) + + def test_localized_meridians_capitalized(self): + parser_ = parser.DateTimeParser("hu_hu") + self.assertEqual( + parser_.parse("2013-01-01 5 DE", "YYYY-MM-DD h A"), datetime(2013, 1, 1, 5) + ) + + self.assertEqual( + parser_.parse("2013-01-01 5 DU", "YYYY-MM-DD h A"), datetime(2013, 1, 1, 17) + ) + + # regression test for issue #607 + def test_es_meridians(self): + parser_ = parser.DateTimeParser("es") + + self.assertEqual( + parser_.parse("Junio 30, 2019 - 08:00 pm", "MMMM DD, YYYY - hh:mm a"), + datetime(2019, 6, 30, 20, 0), + ) + + with self.assertRaises(ParserError): + parser_.parse( + "Junio 30, 2019 - 08:00 pasdfasdfm", "MMMM DD, YYYY - hh:mm a" + ) + + def test_fr_meridians(self): + parser_ = parser.DateTimeParser("fr") + + # the French locale always uses a 24 hour clock, so it does not support meridians + with self.assertRaises(ParserError): + parser_.parse("Janvier 30, 2019 - 08:00 pm", "MMMM DD, YYYY - hh:mm a") + + +class DateTimeParserMonthOrdinalDayTests(Chai): + def setUp(self): + super(DateTimeParserMonthOrdinalDayTests, self).setUp() + + self.parser = parser.DateTimeParser("en_us") + + def test_english(self): + parser_ = parser.DateTimeParser("en_us") + + self.assertEqual( + parser_.parse("January 1st, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) + ) + self.assertEqual( + parser_.parse("January 2nd, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 2) + ) + self.assertEqual( + parser_.parse("January 3rd, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 3) + ) + self.assertEqual( + parser_.parse("January 4th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 4) + ) + self.assertEqual( + parser_.parse("January 11th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 11) + ) + self.assertEqual( + parser_.parse("January 12th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 12) + ) + self.assertEqual( + parser_.parse("January 13th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 13) + ) + self.assertEqual( + parser_.parse("January 21st, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 21) + ) + self.assertEqual( + parser_.parse("January 31st, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 31) + ) + + with self.assertRaises(ParserError): + parser_.parse("January 1th, 2013", "MMMM Do, YYYY") + + with self.assertRaises(ParserError): + parser_.parse("January 11st, 2013", "MMMM Do, YYYY") + + def test_italian(self): + parser_ = parser.DateTimeParser("it_it") + + self.assertEqual( + parser_.parse("Gennaio 1º, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) + ) + + def test_spanish(self): + parser_ = parser.DateTimeParser("es_es") + + self.assertEqual( + parser_.parse("Enero 1º, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) + ) + + def test_french(self): + parser_ = parser.DateTimeParser("fr_fr") + + self.assertEqual( + parser_.parse("Janvier 1er, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) + ) + + self.assertEqual( + parser_.parse("Janvier 2e, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 2) + ) + + self.assertEqual( + parser_.parse("Janvier 11e, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 11) + ) + + +class DateTimeParserSearchDateTests(Chai): + def setUp(self): + super(DateTimeParserSearchDateTests, self).setUp() + self.parser = parser.DateTimeParser() + + def test_parse_search(self): + + self.assertEqual( + 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): + + self.assertEqual( + 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), + ) + + self.assertEqual( + self.parser.parse( + "Call 01-02-03 on 79-01-01 12:05:10", "YY-MM-DD HH:mm:ss" + ), + datetime(1979, 1, 1, 12, 5, 10), + ) + + def test_parse_search_with_names(self): + + self.assertEqual( + self.parser.parse("June was born in May 1980", "MMMM YYYY"), + datetime(1980, 5, 1), + ) + + def test_parse_search_locale_with_names(self): + p = parser.DateTimeParser("sv_se") + + self.assertEqual( + p.parse("Jan föddes den 31 Dec 1980", "DD MMM YYYY"), datetime(1980, 12, 31) + ) + + self.assertEqual( + p.parse("Jag föddes den 25 Augusti 1975", "DD MMMM YYYY"), + datetime(1975, 8, 25), + ) + + def test_parse_search_fails(self): + + with self.assertRaises(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" + self.assertEqual( + self.parser.parse("Thursday, December 10, 2015 at 5:09pm", format), + datetime(2015, 12, 10, 17, 9), + ) + + format = "[MMMM] M D, YYYY [at] h:mma" + self.assertEqual( + self.parser.parse("MMMM 12 10, 2015 at 5:09pm", format), + datetime(2015, 12, 10, 17, 9), + ) + + format = "[It happened on] MMMM Do [in the year] YYYY [a long time ago]" + self.assertEqual( + self.parser.parse( + "It happened on November 25th in the year 1990 a long time ago", format + ), + datetime(1990, 11, 25), + ) + + format = "[It happened on] MMMM Do [in the][ year] YYYY [a long time ago]" + self.assertEqual( + self.parser.parse( + "It happened on November 25th in the year 1990 a long time ago", format + ), + datetime(1990, 11, 25), + ) + + format = "[I'm][ entirely][ escaped,][ weee!]" + self.assertEqual( + self.parser.parse("I'm entirely escaped, weee!", format), datetime(1, 1, 1) + ) + + # Special RegEx characters + format = "MMM DD, YYYY |^${}().*+?<>-& h:mm A" + self.assertEqual( + self.parser.parse("Dec 31, 2017 |^${}().*+?<>-& 2:00 AM", format), + datetime(2017, 12, 31, 2, 0), + ) diff --git a/tests/util_tests.py b/tests/util_tests.py index 804ae62a2..b8a2c1360 100644 --- a/tests/util_tests.py +++ b/tests/util_tests.py @@ -2,39 +2,38 @@ import time from datetime import datetime -from chai import Chai - from arrow import util +import pytest -class UtilTests(Chai): +class TestUtil: def test_total_seconds(self): td = datetime(2019, 1, 1) - datetime(2018, 1, 1) - self.assertEqual(util.total_seconds(td), td.total_seconds()) + assert util.total_seconds(td) == td.total_seconds() def test_is_timestamp(self): timestamp_float = time.time() timestamp_int = int(timestamp_float) - self.assertTrue(util.is_timestamp(timestamp_int)) - self.assertTrue(util.is_timestamp(timestamp_float)) - self.assertTrue(util.is_timestamp(str(timestamp_int))) - self.assertTrue(util.is_timestamp(str(timestamp_float))) + assert util.is_timestamp(timestamp_int) + assert util.is_timestamp(timestamp_float) + assert util.is_timestamp(str(timestamp_int)) + assert util.is_timestamp(str(timestamp_float)) - self.assertFalse(util.is_timestamp(True)) - self.assertFalse(util.is_timestamp(False)) + assert not util.is_timestamp(True) + assert not util.is_timestamp(False) class InvalidTimestamp: pass - self.assertFalse(util.is_timestamp(InvalidTimestamp())) + assert not util.is_timestamp(InvalidTimestamp()) full_datetime = "2019-06-23T13:12:42" - self.assertFalse(util.is_timestamp(full_datetime)) + assert not util.is_timestamp(full_datetime) def test_iso_gregorian(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): util.iso_to_gregorian(2013, 0, 5) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): util.iso_to_gregorian(2013, 8, 0) diff --git a/tests/util_tests.py.bak b/tests/util_tests.py.bak new file mode 100644 index 000000000..804ae62a2 --- /dev/null +++ b/tests/util_tests.py.bak @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +import time +from datetime import datetime + +from chai import Chai + +from arrow import util + + +class UtilTests(Chai): + def test_total_seconds(self): + td = datetime(2019, 1, 1) - datetime(2018, 1, 1) + self.assertEqual(util.total_seconds(td), td.total_seconds()) + + def test_is_timestamp(self): + timestamp_float = time.time() + timestamp_int = int(timestamp_float) + + self.assertTrue(util.is_timestamp(timestamp_int)) + self.assertTrue(util.is_timestamp(timestamp_float)) + self.assertTrue(util.is_timestamp(str(timestamp_int))) + self.assertTrue(util.is_timestamp(str(timestamp_float))) + + self.assertFalse(util.is_timestamp(True)) + self.assertFalse(util.is_timestamp(False)) + + class InvalidTimestamp: + pass + + self.assertFalse(util.is_timestamp(InvalidTimestamp())) + + full_datetime = "2019-06-23T13:12:42" + self.assertFalse(util.is_timestamp(full_datetime)) + + def test_iso_gregorian(self): + with self.assertRaises(ValueError): + util.iso_to_gregorian(2013, 0, 5) + + with self.assertRaises(ValueError): + util.iso_to_gregorian(2013, 8, 0) From 7b4a7b81d78f78c22cc0f3ba39bbef03472e23a7 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 4 Jan 2020 17:00:19 -0500 Subject: [PATCH 373/649] Finished full revamp of test suite to use pytest --- Makefile | 4 +- requirements.txt | 7 +- setup.cfg | 30 +- tests/api_tests.py.bak | 33 - tests/arrow_tests.py.bak | 2081 ----------------- tests/factory_tests.py.bak | 383 --- tests/formatter_tests.py.bak | 199 -- tests/locales_tests.py.bak | 954 -------- tests/parser_tests.py | 1629 ------------- tests/parser_tests.py.bak | 1629 ------------- tests/{api_tests.py => test_api.py} | 0 tests/{arrow_tests.py => test_arrow.py} | 29 +- tests/{factory_tests.py => test_factory.py} | 47 +- .../{formatter_tests.py => test_formatter.py} | 0 tests/{locales_tests.py => test_locales.py} | 20 +- tests/test_parser.py | 1462 ++++++++++++ tests/{util_tests.py => test_util.py} | 3 +- tests/util_tests.py.bak | 40 - tests/utils.py | 7 + tox.ini | 4 +- 20 files changed, 1543 insertions(+), 7018 deletions(-) delete mode 100644 tests/api_tests.py.bak delete mode 100644 tests/arrow_tests.py.bak delete mode 100644 tests/factory_tests.py.bak delete mode 100644 tests/formatter_tests.py.bak delete mode 100644 tests/locales_tests.py.bak delete mode 100644 tests/parser_tests.py delete mode 100644 tests/parser_tests.py.bak rename tests/{api_tests.py => test_api.py} (100%) rename tests/{arrow_tests.py => test_arrow.py} (98%) rename tests/{factory_tests.py => test_factory.py} (89%) rename tests/{formatter_tests.py => test_formatter.py} (100%) rename tests/{locales_tests.py => test_locales.py} (98%) create mode 100644 tests/test_parser.py rename tests/{util_tests.py => test_util.py} (99%) delete mode 100644 tests/util_tests.py.bak diff --git a/Makefile b/Makefile index e7d324d1a..a05957d4a 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,8 @@ build27 build35 build36 build37 build38: venv/bin/pre-commit install test: - rm -f .coverage - . venv/bin/activate && nosetests + rm -f .coverage coverage.xml + . venv/bin/activate && pytest lint: venv/bin/pre-commit run --all-files --show-diff-on-failure diff --git a/requirements.txt b/requirements.txt index 8281e8e91..11c0c72cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,9 @@ backports.functools_lru_cache==1.6.1; python_version == "2.7" -chai==1.1.2 dateparser==0.7.* -mock==3.0.* -nose==1.3.7 -nose-cov==1.6 pre-commit==1.20.* +pytest +pytest-cov +pytest-mock python-dateutil==2.8.* pytz==2019.* simplejson==3.16.* diff --git a/setup.cfg b/setup.cfg index 08cae3ddf..1f7575065 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,31 +1,15 @@ -[nosetests] -where = tests -verbosity = 2 -all-modules = true -with-coverage = true -cover-min-percentage = 100 -cover-package = arrow -cover-erase = true - -[coverage:run] -branch = true -source = - arrow - tests - -[coverage:report] -show_missing = true -fail_under = 100 - -[flake8] -per-file-ignores = arrow/__init__.py:F401 -ignore = E203,E501,W503 +[tool:pytest] +# addopts = -v --nf --cov-branch --cov=arrow tests --cov-fail-under=100 --cov-report=term-missing --cov-report=xml [tool:isort] line_length = 88 multi_line_output = 3 include_trailing_comma = true -known_third_party = chai,dateparser,dateutil,mock,pytz,setuptools,simplejson +known_third_party = dateparser,dateutil,pytest,pytz,setuptools,simplejson + +[flake8] +per-file-ignores = arrow/__init__.py:F401 +ignore = E203,E501,W503 [bdist_wheel] universal = 1 diff --git a/tests/api_tests.py.bak b/tests/api_tests.py.bak deleted file mode 100644 index 1cf171653..000000000 --- a/tests/api_tests.py.bak +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -from chai import Chai - -from arrow import api, arrow, factory - - -class ModuleTests(Chai): - def test_get(self): - - self.expect(api._factory.get).args(1, b=2).returns("result") - - self.assertEqual(api.get(1, b=2), "result") - - def test_utcnow(self): - - self.expect(api._factory.utcnow).returns("utcnow") - - self.assertEqual(api.utcnow(), "utcnow") - - def test_now(self): - - self.expect(api._factory.now).args("tz").returns("now") - - self.assertEqual(api.now("tz"), "now") - - def test_factory(self): - class MockCustomArrowClass(arrow.Arrow): - pass - - result = api.factory(MockCustomArrowClass) - - self.assertIsInstance(result, factory.ArrowFactory) - self.assertIsInstance(result.utcnow(), MockCustomArrowClass) diff --git a/tests/arrow_tests.py.bak b/tests/arrow_tests.py.bak deleted file mode 100644 index 9d6827660..000000000 --- a/tests/arrow_tests.py.bak +++ /dev/null @@ -1,2081 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -import calendar -import pickle -import sys -import time -from datetime import date, datetime, timedelta - -import pytz -import simplejson as json -from chai import Chai -from dateutil import tz -from dateutil.relativedelta import FR, MO, SA, SU, TH, TU, WE -from mock import patch - -from arrow import arrow, util - - -def assertDtEqual(dt1, dt2, within=10): - assertEqual(dt1.tzinfo, dt2.tzinfo) # noqa: F821 - assertTrue(abs(util.total_seconds(dt1 - dt2)) < within) # noqa: F821 - - -class ArrowInitTests(Chai): - def test_init_bad_input(self): - - with self.assertRaises(TypeError): - arrow.Arrow(2013) - - with self.assertRaises(TypeError): - arrow.Arrow(2013, 2) - - with self.assertRaises(ValueError): - 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()) - self.assertEqual(result._datetime, self.expected) - - result = arrow.Arrow(2013, 2, 2, 12) - self.expected = datetime(2013, 2, 2, 12, tzinfo=tz.tzutc()) - self.assertEqual(result._datetime, self.expected) - - result = arrow.Arrow(2013, 2, 2, 12, 30) - self.expected = datetime(2013, 2, 2, 12, 30, tzinfo=tz.tzutc()) - self.assertEqual(result._datetime, self.expected) - - result = arrow.Arrow(2013, 2, 2, 12, 30, 45) - self.expected = datetime(2013, 2, 2, 12, 30, 45, tzinfo=tz.tzutc()) - self.assertEqual(result._datetime, self.expected) - - result = arrow.Arrow(2013, 2, 2, 12, 30, 45, 999999) - self.expected = datetime(2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.tzutc()) - self.assertEqual(result._datetime, self.expected) - - result = arrow.Arrow( - 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") - ) - self.expected = datetime( - 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") - ) - self.assertEqual(result._datetime, self.expected) - - # 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") - ) - self.expected = datetime( - 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") - ) - self.assertEqual(result._datetime, self.expected) - assertDtEqual(result._datetime, self.expected, 1) - - -class ArrowFactoryTests(Chai): - def test_now(self): - - result = arrow.Arrow.now() - - assertDtEqual(result._datetime, datetime.now().replace(tzinfo=tz.tzlocal())) - - def test_utcnow(self): - - result = arrow.Arrow.utcnow() - - assertDtEqual(result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc())) - - def test_fromtimestamp(self): - - timestamp = time.time() - - result = arrow.Arrow.fromtimestamp(timestamp) - assertDtEqual(result._datetime, datetime.now().replace(tzinfo=tz.tzlocal())) - - result = arrow.Arrow.fromtimestamp(timestamp, tzinfo=tz.gettz("Europe/Paris")) - assertDtEqual( - result._datetime, - datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), - ) - - result = arrow.Arrow.fromtimestamp(timestamp, tzinfo="Europe/Paris") - assertDtEqual( - result._datetime, - datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), - ) - - with self.assertRaises(ValueError): - arrow.Arrow.fromtimestamp("invalid timestamp") - - def test_utcfromtimestamp(self): - - timestamp = time.time() - - result = arrow.Arrow.utcfromtimestamp(timestamp) - assertDtEqual(result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc())) - - with self.assertRaises(ValueError): - arrow.Arrow.utcfromtimestamp("invalid timestamp") - - def test_fromdatetime(self): - - dt = datetime(2013, 2, 3, 12, 30, 45, 1) - - result = arrow.Arrow.fromdatetime(dt) - - self.assertEqual(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) - - self.assertEqual(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")) - - self.assertEqual(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")) - - self.assertEqual( - 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") - self.assertEqual( - 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") - ) - self.assertEqual( - result._datetime, - datetime(2013, 2, 3, 12, 30, 45, tzinfo=tz.gettz("Europe/Paris")), - ) - - -class ArrowRepresentationTests(Chai): - def setUp(self): - super(ArrowRepresentationTests, self).setUp() - - self.arrow = arrow.Arrow(2013, 2, 3, 12, 30, 45, 1) - - def test_repr(self): - - result = self.arrow.__repr__() - - self.assertEqual( - result, "".format(self.arrow._datetime.isoformat()) - ) - - def test_str(self): - - result = self.arrow.__str__() - - self.assertEqual(result, self.arrow._datetime.isoformat()) - - def test_hash(self): - - result = self.arrow.__hash__() - - self.assertEqual(result, self.arrow._datetime.__hash__()) - - def test_format(self): - - result = "{:YYYY-MM-DD}".format(self.arrow) - - self.assertEqual(result, "2013-02-03") - - def test_bare_format(self): - - result = self.arrow.format() - - self.assertEqual(result, "2013-02-03 12:30:45+00:00") - - def test_format_no_format_string(self): - - result = "{}".format(self.arrow) - - self.assertEqual(result, str(self.arrow)) - - def test_clone(self): - - result = self.arrow.clone() - - self.assertTrue(result is not self.arrow) - self.assertEqual(result._datetime, self.arrow._datetime) - - -class ArrowAttributeTests(Chai): - def setUp(self): - super(ArrowAttributeTests, self).setUp() - - self.arrow = arrow.Arrow(2013, 1, 1) - - def test_getattr_base(self): - - with self.assertRaises(AttributeError): - self.arrow.prop - - def test_getattr_week(self): - - self.assertEqual(self.arrow.week, 1) - - def test_getattr_quarter(self): - # start dates - q1 = arrow.Arrow(2013, 1, 1) - q2 = arrow.Arrow(2013, 4, 1) - q3 = arrow.Arrow(2013, 8, 1) - q4 = arrow.Arrow(2013, 10, 1) - self.assertEqual(q1.quarter, 1) - self.assertEqual(q2.quarter, 2) - self.assertEqual(q3.quarter, 3) - self.assertEqual(q4.quarter, 4) - - # end dates - q1 = arrow.Arrow(2013, 3, 31) - q2 = arrow.Arrow(2013, 6, 30) - q3 = arrow.Arrow(2013, 9, 30) - q4 = arrow.Arrow(2013, 12, 31) - self.assertEqual(q1.quarter, 1) - self.assertEqual(q2.quarter, 2) - self.assertEqual(q3.quarter, 3) - self.assertEqual(q4.quarter, 4) - - def test_getattr_dt_value(self): - - self.assertEqual(self.arrow.year, 2013) - - def test_tzinfo(self): - - self.arrow.tzinfo = tz.gettz("PST") - self.assertEqual(self.arrow.tzinfo, tz.gettz("PST")) - - def test_naive(self): - - self.assertEqual(self.arrow.naive, self.arrow._datetime.replace(tzinfo=None)) - - def test_timestamp(self): - - self.assertEqual( - self.arrow.timestamp, calendar.timegm(self.arrow._datetime.utctimetuple()) - ) - - def test_float_timestamp(self): - - result = self.arrow.float_timestamp - self.arrow.timestamp - - self.assertEqual(result, self.arrow.microsecond) - - -class ArrowComparisonTests(Chai): - def setUp(self): - super(ArrowComparisonTests, self).setUp() - - self.arrow = arrow.Arrow.utcnow() - - def test_eq(self): - - self.assertTrue(self.arrow == self.arrow) - self.assertTrue(self.arrow == self.arrow.datetime) - self.assertFalse(self.arrow == "abc") - - def test_ne(self): - - self.assertFalse(self.arrow != self.arrow) - self.assertFalse(self.arrow != self.arrow.datetime) - self.assertTrue(self.arrow != "abc") - - def test_gt(self): - - arrow_cmp = self.arrow.shift(minutes=1) - - self.assertFalse(self.arrow > self.arrow) - self.assertFalse(self.arrow > self.arrow.datetime) - - with self.assertRaises(TypeError): - self.arrow > "abc" - - self.assertTrue(self.arrow < arrow_cmp) - self.assertTrue(self.arrow < arrow_cmp.datetime) - - def test_ge(self): - - with self.assertRaises(TypeError): - self.arrow >= "abc" - - self.assertTrue(self.arrow >= self.arrow) - self.assertTrue(self.arrow >= self.arrow.datetime) - - def test_lt(self): - - arrow_cmp = self.arrow.shift(minutes=1) - - self.assertFalse(self.arrow < self.arrow) - self.assertFalse(self.arrow < self.arrow.datetime) - - with self.assertRaises(TypeError): - self.arrow < "abc" - - self.assertTrue(self.arrow < arrow_cmp) - self.assertTrue(self.arrow < arrow_cmp.datetime) - - def test_le(self): - - with self.assertRaises(TypeError): - self.arrow <= "abc" - - self.assertTrue(self.arrow <= self.arrow) - self.assertTrue(self.arrow <= self.arrow.datetime) - - -class ArrowMathTests(Chai): - def setUp(self): - super(ArrowMathTests, self).setUp() - - self.arrow = arrow.Arrow(2013, 1, 1) - - def test_add_timedelta(self): - - result = self.arrow.__add__(timedelta(days=1)) - - self.assertEqual(result._datetime, datetime(2013, 1, 2, tzinfo=tz.tzutc())) - - def test_add_other(self): - - with self.assertRaises(TypeError): - self.arrow + 1 - - def test_radd(self): - - result = self.arrow.__radd__(timedelta(days=1)) - - self.assertEqual(result._datetime, datetime(2013, 1, 2, tzinfo=tz.tzutc())) - - def test_sub_timedelta(self): - - result = self.arrow.__sub__(timedelta(days=1)) - - self.assertEqual(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())) - - self.assertEqual(result, timedelta(days=11)) - - def test_sub_arrow(self): - - result = self.arrow.__sub__(arrow.Arrow(2012, 12, 21, tzinfo=tz.tzutc())) - - self.assertEqual(result, timedelta(days=11)) - - def test_sub_other(self): - - with self.assertRaises(TypeError): - self.arrow - object() - - def test_rsub_datetime(self): - - result = self.arrow.__rsub__(datetime(2012, 12, 21, tzinfo=tz.tzutc())) - - self.assertEqual(result, timedelta(days=-11)) - - def test_rsub_other(self): - - with self.assertRaises(TypeError): - timedelta(days=1) - self.arrow - - -class ArrowDatetimeInterfaceTests(Chai): - def setUp(self): - super(ArrowDatetimeInterfaceTests, self).setUp() - - self.arrow = arrow.Arrow.utcnow() - - def test_date(self): - - result = self.arrow.date() - - self.assertEqual(result, self.arrow._datetime.date()) - - def test_time(self): - - result = self.arrow.time() - - self.assertEqual(result, self.arrow._datetime.time()) - - def test_timetz(self): - - result = self.arrow.timetz() - - self.assertEqual(result, self.arrow._datetime.timetz()) - - def test_astimezone(self): - - other_tz = tz.gettz("US/Pacific") - - result = self.arrow.astimezone(other_tz) - - self.assertEqual(result, self.arrow._datetime.astimezone(other_tz)) - - def test_utcoffset(self): - - result = self.arrow.utcoffset() - - self.assertEqual(result, self.arrow._datetime.utcoffset()) - - def test_dst(self): - - result = self.arrow.dst() - - self.assertEqual(result, self.arrow._datetime.dst()) - - def test_timetuple(self): - - result = self.arrow.timetuple() - - self.assertEqual(result, self.arrow._datetime.timetuple()) - - def test_utctimetuple(self): - - result = self.arrow.utctimetuple() - - self.assertEqual(result, self.arrow._datetime.utctimetuple()) - - def test_toordinal(self): - - result = self.arrow.toordinal() - - self.assertEqual(result, self.arrow._datetime.toordinal()) - - def test_weekday(self): - - result = self.arrow.weekday() - - self.assertEqual(result, self.arrow._datetime.weekday()) - - def test_isoweekday(self): - - result = self.arrow.isoweekday() - - self.assertEqual(result, self.arrow._datetime.isoweekday()) - - def test_isocalendar(self): - - result = self.arrow.isocalendar() - - self.assertEqual(result, self.arrow._datetime.isocalendar()) - - def test_isoformat(self): - - result = self.arrow.isoformat() - - self.assertEqual(result, self.arrow._datetime.isoformat()) - - def test_simplejson(self): - - result = json.dumps({"v": self.arrow.for_json()}, for_json=True) - - self.assertEqual(json.loads(result)["v"], self.arrow._datetime.isoformat()) - - def test_ctime(self): - - result = self.arrow.ctime() - - self.assertEqual(result, self.arrow._datetime.ctime()) - - def test_strftime(self): - - result = self.arrow.strftime("%Y") - - self.assertEqual(result, self.arrow._datetime.strftime("%Y")) - - -class ArrowFalsePositiveDstTests(Chai): - """These tests relate to issues #376 and #551. - The key points in both issues are that arrow will assign a UTC timezone if none is provided and - .to() will change other attributes to be correct whereas .replace() only changes the specified attribute. - - Issue 376 - >>> arrow.get('2016-11-06').to('America/New_York').ceil('day') - < Arrow [2016-11-05T23:59:59.999999-04:00] > - - Issue 551 - >>> just_before = arrow.get('2018-11-04T01:59:59.999999') - >>> just_before - 2018-11-04T01:59:59.999999+00:00 - >>> just_after = just_before.shift(microseconds=1) - >>> just_after - 2018-11-04T02:00:00+00:00 - >>> just_before_eastern = just_before.replace(tzinfo='US/Eastern') - >>> just_before_eastern - 2018-11-04T01:59:59.999999-04:00 - >>> just_after_eastern = just_after.replace(tzinfo='US/Eastern') - >>> just_after_eastern - 2018-11-04T02:00:00-05:00 - """ - - def setUp(self): - - super(ArrowFalsePositiveDstTests, self).setUp() - self.before_1 = arrow.Arrow( - 2016, 11, 6, 3, 59, tzinfo=tz.gettz("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.after_2 = arrow.Arrow( - 2016, 11, 6, 23, 59, tzinfo=tz.gettz("America/New_York") - ) - self.before_3 = arrow.Arrow( - 2018, 11, 4, 3, 59, tzinfo=tz.gettz("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.after_4 = arrow.Arrow( - 2018, 11, 4, 23, 59, tzinfo=tz.gettz("America/New_York") - ) - - def test_dst(self): - - self.assertEqual(self.before_1.day, self.before_2.day) - self.assertEqual(self.after_1.day, self.after_2.day) - self.assertEqual(self.before_3.day, self.before_4.day) - self.assertEqual(self.after_3.day, self.after_4.day) - - -class ArrowConversionTests(Chai): - def test_to(self): - - dt_from = datetime.now() - arrow_from = arrow.Arrow.fromdatetime(dt_from, tz.gettz("US/Pacific")) - - self.expected = dt_from.replace(tzinfo=tz.gettz("US/Pacific")).astimezone( - tz.tzutc() - ) - - self.assertEqual(arrow_from.to("UTC").datetime, self.expected) - self.assertEqual(arrow_from.to(tz.tzutc()).datetime, self.expected) - - -class ArrowPicklingTests(Chai): - def test_pickle_and_unpickle(self): - - dt = arrow.Arrow.utcnow() - - pickled = pickle.dumps(dt) - - unpickled = pickle.loads(pickled) - - self.assertEqual(unpickled, dt) - - -class ArrowReplaceTests(Chai): - def test_not_attr(self): - - with self.assertRaises(AttributeError): - arrow.Arrow.utcnow().replace(abc=1) - - def test_replace(self): - - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) - - self.assertEqual(arw.replace(year=2012), arrow.Arrow(2012, 5, 5, 12, 30, 45)) - self.assertEqual(arw.replace(month=1), arrow.Arrow(2013, 1, 5, 12, 30, 45)) - self.assertEqual(arw.replace(day=1), arrow.Arrow(2013, 5, 1, 12, 30, 45)) - self.assertEqual(arw.replace(hour=1), arrow.Arrow(2013, 5, 5, 1, 30, 45)) - self.assertEqual(arw.replace(minute=1), arrow.Arrow(2013, 5, 5, 12, 1, 45)) - self.assertEqual(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")) - - self.assertEqual(result, arw.datetime.replace(tzinfo=tz.gettz("US/Pacific"))) - - def test_replace_week(self): - - with self.assertRaises(AttributeError): - arrow.Arrow.utcnow().replace(week=1) - - def test_replace_quarter(self): - - with self.assertRaises(AttributeError): - arrow.Arrow.utcnow().replace(quarter=1) - - def test_replace_other_kwargs(self): - - with self.assertRaises(AttributeError): - arrow.utcnow().replace(abc="def") - - -class ArrowShiftTests(Chai): - def test_not_attr(self): - - now = arrow.Arrow.utcnow() - - with self.assertRaises(AttributeError): - now.shift(abc=1) - - with self.assertRaises(AttributeError): - now.shift(week=1) - - def test_shift(self): - - arw = arrow.Arrow(2013, 5, 5, 12, 30, 45) - - self.assertEqual(arw.shift(years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45)) - self.assertEqual(arw.shift(quarters=1), arrow.Arrow(2013, 8, 5, 12, 30, 45)) - self.assertEqual( - arw.shift(quarters=1, months=1), arrow.Arrow(2013, 9, 5, 12, 30, 45) - ) - self.assertEqual(arw.shift(months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45)) - self.assertEqual(arw.shift(weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45)) - self.assertEqual(arw.shift(days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - self.assertEqual(arw.shift(hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45)) - self.assertEqual(arw.shift(minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45)) - self.assertEqual(arw.shift(seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46)) - self.assertEqual( - arw.shift(microseconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 45, 1) - ) - - # Remember: Python's weekday 0 is Monday - self.assertEqual(arw.shift(weekday=0), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=1), arrow.Arrow(2013, 5, 7, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=2), arrow.Arrow(2013, 5, 8, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=3), arrow.Arrow(2013, 5, 9, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=4), arrow.Arrow(2013, 5, 10, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=5), arrow.Arrow(2013, 5, 11, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=6), arw) - - with self.assertRaises(IndexError): - arw.shift(weekday=7) - - # Use dateutil.relativedelta's convenient day instances - self.assertEqual(arw.shift(weekday=MO), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=MO(0)), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=MO(1)), arrow.Arrow(2013, 5, 6, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=MO(2)), arrow.Arrow(2013, 5, 13, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TU), arrow.Arrow(2013, 5, 7, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TU(0)), arrow.Arrow(2013, 5, 7, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TU(1)), arrow.Arrow(2013, 5, 7, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TU(2)), arrow.Arrow(2013, 5, 14, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=WE), arrow.Arrow(2013, 5, 8, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=WE(0)), arrow.Arrow(2013, 5, 8, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=WE(1)), arrow.Arrow(2013, 5, 8, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=WE(2)), arrow.Arrow(2013, 5, 15, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TH), arrow.Arrow(2013, 5, 9, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TH(0)), arrow.Arrow(2013, 5, 9, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TH(1)), arrow.Arrow(2013, 5, 9, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TH(2)), arrow.Arrow(2013, 5, 16, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=FR), arrow.Arrow(2013, 5, 10, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=FR(0)), arrow.Arrow(2013, 5, 10, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=FR(1)), arrow.Arrow(2013, 5, 10, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=FR(2)), arrow.Arrow(2013, 5, 17, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=SA), arrow.Arrow(2013, 5, 11, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=SA(0)), arrow.Arrow(2013, 5, 11, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=SA(1)), arrow.Arrow(2013, 5, 11, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=SA(2)), arrow.Arrow(2013, 5, 18, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=SU), arw) - self.assertEqual(arw.shift(weekday=SU(0)), arw) - self.assertEqual(arw.shift(weekday=SU(1)), arw) - self.assertEqual(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) - - self.assertEqual(arw.shift(years=-1), arrow.Arrow(2012, 5, 5, 12, 30, 45)) - self.assertEqual(arw.shift(quarters=-1), arrow.Arrow(2013, 2, 5, 12, 30, 45)) - self.assertEqual( - arw.shift(quarters=-1, months=-1), arrow.Arrow(2013, 1, 5, 12, 30, 45) - ) - self.assertEqual(arw.shift(months=-1), arrow.Arrow(2013, 4, 5, 12, 30, 45)) - self.assertEqual(arw.shift(weeks=-1), arrow.Arrow(2013, 4, 28, 12, 30, 45)) - self.assertEqual(arw.shift(days=-1), arrow.Arrow(2013, 5, 4, 12, 30, 45)) - self.assertEqual(arw.shift(hours=-1), arrow.Arrow(2013, 5, 5, 11, 30, 45)) - self.assertEqual(arw.shift(minutes=-1), arrow.Arrow(2013, 5, 5, 12, 29, 45)) - self.assertEqual(arw.shift(seconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44)) - self.assertEqual( - arw.shift(microseconds=-1), arrow.Arrow(2013, 5, 5, 12, 30, 44, 999999) - ) - - # Not sure how practical these negative weekdays are - self.assertEqual(arw.shift(weekday=-1), arw.shift(weekday=SU)) - self.assertEqual(arw.shift(weekday=-2), arw.shift(weekday=SA)) - self.assertEqual(arw.shift(weekday=-3), arw.shift(weekday=FR)) - self.assertEqual(arw.shift(weekday=-4), arw.shift(weekday=TH)) - self.assertEqual(arw.shift(weekday=-5), arw.shift(weekday=WE)) - self.assertEqual(arw.shift(weekday=-6), arw.shift(weekday=TU)) - self.assertEqual(arw.shift(weekday=-7), arw.shift(weekday=MO)) - - with self.assertRaises(IndexError): - arw.shift(weekday=-8) - - self.assertEqual( - arw.shift(weekday=MO(-1)), arrow.Arrow(2013, 4, 29, 12, 30, 45) - ) - self.assertEqual( - arw.shift(weekday=TU(-1)), arrow.Arrow(2013, 4, 30, 12, 30, 45) - ) - self.assertEqual(arw.shift(weekday=WE(-1)), arrow.Arrow(2013, 5, 1, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=TH(-1)), arrow.Arrow(2013, 5, 2, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=FR(-1)), arrow.Arrow(2013, 5, 3, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=SA(-1)), arrow.Arrow(2013, 5, 4, 12, 30, 45)) - self.assertEqual(arw.shift(weekday=SU(-1)), arw) - self.assertEqual( - 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. - # Recall that the keyword argument dict, like all dicts, is unordered, so only certain - # combinations of arguments would exhibit this. - self.assertEqual( - arw.shift(quarters=0, years=1), arrow.Arrow(2014, 5, 5, 12, 30, 45) - ) - self.assertEqual( - arw.shift(quarters=0, months=1), arrow.Arrow(2013, 6, 5, 12, 30, 45) - ) - self.assertEqual( - arw.shift(quarters=0, weeks=1), arrow.Arrow(2013, 5, 12, 12, 30, 45) - ) - self.assertEqual( - arw.shift(quarters=0, days=1), arrow.Arrow(2013, 5, 6, 12, 30, 45) - ) - self.assertEqual( - arw.shift(quarters=0, hours=1), arrow.Arrow(2013, 5, 5, 13, 30, 45) - ) - self.assertEqual( - arw.shift(quarters=0, minutes=1), arrow.Arrow(2013, 5, 5, 12, 31, 45) - ) - self.assertEqual( - arw.shift(quarters=0, seconds=1), arrow.Arrow(2013, 5, 5, 12, 30, 46) - ) - self.assertEqual( - arw.shift(quarters=0, microseconds=1), - arrow.Arrow(2013, 5, 5, 12, 30, 45, 1), - ) - - -class ArrowRangeTests(Chai): - def test_year(self): - - result = list( - arrow.Arrow.range( - "year", datetime(2013, 1, 2, 3, 4, 5), datetime(2016, 4, 5, 6, 7, 8) - ) - ) - - self.assertEqual( - result, - [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2014, 1, 2, 3, 4, 5), - arrow.Arrow(2015, 1, 2, 3, 4, 5), - arrow.Arrow(2016, 1, 2, 3, 4, 5), - ], - ) - - def test_quarter(self): - - result = list( - arrow.Arrow.range( - "quarter", datetime(2013, 2, 3, 4, 5, 6), datetime(2013, 5, 6, 7, 8, 9) - ) - ) - - self.assertEqual( - result, [arrow.Arrow(2013, 2, 3, 4, 5, 6), arrow.Arrow(2013, 5, 3, 4, 5, 6)] - ) - - def test_month(self): - - result = list( - arrow.Arrow.range( - "month", datetime(2013, 2, 3, 4, 5, 6), datetime(2013, 5, 6, 7, 8, 9) - ) - ) - - self.assertEqual( - result, - [ - arrow.Arrow(2013, 2, 3, 4, 5, 6), - arrow.Arrow(2013, 3, 3, 4, 5, 6), - arrow.Arrow(2013, 4, 3, 4, 5, 6), - arrow.Arrow(2013, 5, 3, 4, 5, 6), - ], - ) - - def test_week(self): - - result = list( - arrow.Arrow.range( - "week", datetime(2013, 9, 1, 2, 3, 4), datetime(2013, 10, 1, 2, 3, 4) - ) - ) - - self.assertEqual( - result, - [ - arrow.Arrow(2013, 9, 1, 2, 3, 4), - arrow.Arrow(2013, 9, 8, 2, 3, 4), - arrow.Arrow(2013, 9, 15, 2, 3, 4), - arrow.Arrow(2013, 9, 22, 2, 3, 4), - arrow.Arrow(2013, 9, 29, 2, 3, 4), - ], - ) - - def test_day(self): - - result = list( - arrow.Arrow.range( - "day", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 5, 6, 7, 8) - ) - ) - - self.assertEqual( - result, - [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 3, 3, 4, 5), - arrow.Arrow(2013, 1, 4, 3, 4, 5), - arrow.Arrow(2013, 1, 5, 3, 4, 5), - ], - ) - - def test_hour(self): - - result = list( - arrow.Arrow.range( - "hour", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 6, 7, 8) - ) - ) - - self.assertEqual( - result, - [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 2, 4, 4, 5), - arrow.Arrow(2013, 1, 2, 5, 4, 5), - arrow.Arrow(2013, 1, 2, 6, 4, 5), - ], - ) - - result = list( - arrow.Arrow.range( - "hour", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 3, 4, 5) - ) - ) - - self.assertEqual(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) - ) - ) - - self.assertEqual( - result, - [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 2, 3, 5, 5), - arrow.Arrow(2013, 1, 2, 3, 6, 5), - arrow.Arrow(2013, 1, 2, 3, 7, 5), - ], - ) - - def test_second(self): - - result = list( - arrow.Arrow.range( - "second", datetime(2013, 1, 2, 3, 4, 5), datetime(2013, 1, 2, 3, 4, 8) - ) - ) - - self.assertEqual( - result, - [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 2, 3, 4, 6), - arrow.Arrow(2013, 1, 2, 3, 4, 7), - arrow.Arrow(2013, 1, 2, 3, 4, 8), - ], - ) - - def test_arrow(self): - - result = list( - arrow.Arrow.range( - "day", - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 5, 6, 7, 8), - ) - ) - - self.assertEqual( - result, - [ - arrow.Arrow(2013, 1, 2, 3, 4, 5), - arrow.Arrow(2013, 1, 3, 3, 4, 5), - arrow.Arrow(2013, 1, 4, 3, 4, 5), - arrow.Arrow(2013, 1, 5, 3, 4, 5), - ], - ) - - def test_naive_tz(self): - - result = arrow.Arrow.range( - "year", datetime(2013, 1, 2, 3), datetime(2016, 4, 5, 6), "US/Pacific" - ) - - [self.assertEqual(r.tzinfo, tz.gettz("US/Pacific")) for r in result] - - 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")), - ) - - [self.assertEqual(r.tzinfo, tz.gettz("US/Pacific")) for r in result] - - 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")), - ) - - [self.assertEqual(r.tzinfo, tz.gettz("US/Eastern")) for r in result] - - 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"), - ) - - [self.assertEqual(r.tzinfo, tz.gettz("US/Central")) for r in result] - - def test_unsupported(self): - - with self.assertRaises(AttributeError): - next(arrow.Arrow.range("abc", datetime.utcnow(), datetime.utcnow())) - - -class ArrowSpanRangeTests(Chai): - def test_year(self): - - result = list( - arrow.Arrow.span_range("year", datetime(2013, 2, 1), datetime(2016, 3, 31)) - ) - - self.assertEqual( - result, - [ - ( - arrow.Arrow(2013, 1, 1), - arrow.Arrow(2013, 12, 31, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2014, 1, 1), - arrow.Arrow(2014, 12, 31, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2015, 1, 1), - arrow.Arrow(2015, 12, 31, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2016, 1, 1), - arrow.Arrow(2016, 12, 31, 23, 59, 59, 999999), - ), - ], - ) - - def test_quarter(self): - - result = list( - arrow.Arrow.span_range( - "quarter", datetime(2013, 2, 2), datetime(2013, 5, 15) - ) - ) - - self.assertEqual( - result, - [ - (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 6, 30, 23, 59, 59, 999999)), - ], - ) - - def test_month(self): - - result = list( - arrow.Arrow.span_range("month", datetime(2013, 1, 2), datetime(2013, 4, 15)) - ) - - self.assertEqual( - result, - [ - (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 1, 31, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 2, 1), arrow.Arrow(2013, 2, 28, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 3, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 4, 30, 23, 59, 59, 999999)), - ], - ) - - def test_week(self): - - result = list( - arrow.Arrow.span_range("week", datetime(2013, 2, 2), datetime(2013, 2, 28)) - ) - - self.assertEqual( - result, - [ - (arrow.Arrow(2013, 1, 28), arrow.Arrow(2013, 2, 3, 23, 59, 59, 999999)), - (arrow.Arrow(2013, 2, 4), arrow.Arrow(2013, 2, 10, 23, 59, 59, 999999)), - ( - arrow.Arrow(2013, 2, 11), - arrow.Arrow(2013, 2, 17, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 2, 18), - arrow.Arrow(2013, 2, 24, 23, 59, 59, 999999), - ), - (arrow.Arrow(2013, 2, 25), arrow.Arrow(2013, 3, 3, 23, 59, 59, 999999)), - ], - ) - - def test_day(self): - - result = list( - arrow.Arrow.span_range( - "day", datetime(2013, 1, 1, 12), datetime(2013, 1, 4, 12) - ) - ) - - self.assertEqual( - result, - [ - ( - arrow.Arrow(2013, 1, 1, 0), - arrow.Arrow(2013, 1, 1, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 2, 0), - arrow.Arrow(2013, 1, 2, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 3, 0), - arrow.Arrow(2013, 1, 3, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 4, 0), - arrow.Arrow(2013, 1, 4, 23, 59, 59, 999999), - ), - ], - ) - - def test_days(self): - - result = list( - arrow.Arrow.span_range( - "days", datetime(2013, 1, 1, 12), datetime(2013, 1, 4, 12) - ) - ) - - self.assertEqual( - result, - [ - ( - arrow.Arrow(2013, 1, 1, 0), - arrow.Arrow(2013, 1, 1, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 2, 0), - arrow.Arrow(2013, 1, 2, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 3, 0), - arrow.Arrow(2013, 1, 3, 23, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 4, 0), - arrow.Arrow(2013, 1, 4, 23, 59, 59, 999999), - ), - ], - ) - - def test_hour(self): - - result = list( - arrow.Arrow.span_range( - "hour", datetime(2013, 1, 1, 0, 30), datetime(2013, 1, 1, 3, 30) - ) - ) - - self.assertEqual( - result, - [ - ( - arrow.Arrow(2013, 1, 1, 0), - arrow.Arrow(2013, 1, 1, 0, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 1), - arrow.Arrow(2013, 1, 1, 1, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 2), - arrow.Arrow(2013, 1, 1, 2, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 3), - arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999), - ), - ], - ) - - result = list( - arrow.Arrow.span_range( - "hour", datetime(2013, 1, 1, 3, 30), datetime(2013, 1, 1, 3, 30) - ) - ) - - self.assertEqual( - result, - [(arrow.Arrow(2013, 1, 1, 3), arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999))], - ) - - 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) - ) - ) - - self.assertEqual( - result, - [ - ( - arrow.Arrow(2013, 1, 1, 0, 0), - arrow.Arrow(2013, 1, 1, 0, 0, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 1), - arrow.Arrow(2013, 1, 1, 0, 1, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 2), - arrow.Arrow(2013, 1, 1, 0, 2, 59, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 3), - arrow.Arrow(2013, 1, 1, 0, 3, 59, 999999), - ), - ], - ) - - def test_second(self): - - result = list( - arrow.Arrow.span_range( - "second", datetime(2013, 1, 1), datetime(2013, 1, 1, 0, 0, 3) - ) - ) - - self.assertEqual( - result, - [ - ( - arrow.Arrow(2013, 1, 1, 0, 0, 0), - arrow.Arrow(2013, 1, 1, 0, 0, 0, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 0, 1), - arrow.Arrow(2013, 1, 1, 0, 0, 1, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 0, 2), - arrow.Arrow(2013, 1, 1, 0, 0, 2, 999999), - ), - ( - arrow.Arrow(2013, 1, 1, 0, 0, 3), - arrow.Arrow(2013, 1, 1, 0, 0, 3, 999999), - ), - ], - ) - - def test_naive_tz(self): - - tzinfo = tz.gettz("US/Pacific") - - result = arrow.Arrow.span_range( - "hour", datetime(2013, 1, 1, 0), datetime(2013, 1, 1, 3, 59), "US/Pacific" - ) - - for f, c in result: - self.assertEqual(f.tzinfo, tzinfo) - self.assertEqual(c.tzinfo, tzinfo) - - def test_aware_same_tz(self): - - tzinfo = tz.gettz("US/Pacific") - - result = arrow.Arrow.span_range( - "hour", - datetime(2013, 1, 1, 0, tzinfo=tzinfo), - datetime(2013, 1, 1, 2, 59, tzinfo=tzinfo), - ) - - for f, c in result: - self.assertEqual(f.tzinfo, tzinfo) - self.assertEqual(c.tzinfo, tzinfo) - - def test_aware_different_tz(self): - - tzinfo1 = tz.gettz("US/Pacific") - tzinfo2 = tz.gettz("US/Eastern") - - result = arrow.Arrow.span_range( - "hour", - datetime(2013, 1, 1, 0, tzinfo=tzinfo1), - datetime(2013, 1, 1, 2, 59, tzinfo=tzinfo2), - ) - - for f, c in result: - self.assertEqual(f.tzinfo, tzinfo1) - self.assertEqual(c.tzinfo, tzinfo1) - - 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")), - tz="US/Central", - ) - - for f, c in result: - self.assertEqual(f.tzinfo, tz.gettz("US/Central")) - self.assertEqual(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="[]" - ) - ) - - self.assertEqual( - result, - [ - (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 4, 1)), - (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 7, 1)), - ], - ) - - -class ArrowIntervalTests(Chai): - def test_incorrect_input(self): - correct = True - try: - list( - arrow.Arrow.interval( - "month", datetime(2013, 1, 2), datetime(2013, 4, 15), 0 - ) - ) - except: # noqa: E722 - correct = False - - self.assertEqual(correct, False) - - def test_correct(self): - result = list( - arrow.Arrow.interval( - "hour", datetime(2013, 5, 5, 12, 30), datetime(2013, 5, 5, 17, 15), 2 - ) - ) - - self.assertEqual( - result, - [ - ( - arrow.Arrow(2013, 5, 5, 12), - arrow.Arrow(2013, 5, 5, 13, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 5, 5, 14), - arrow.Arrow(2013, 5, 5, 15, 59, 59, 999999), - ), - ( - arrow.Arrow(2013, 5, 5, 16), - arrow.Arrow(2013, 5, 5, 17, 59, 59, 999999), - ), - ], - ) - - def test_bounds_param_is_passed(self): - result = list( - arrow.Arrow.interval( - "hour", - datetime(2013, 5, 5, 12, 30), - datetime(2013, 5, 5, 17, 15), - 2, - bounds="[]", - ) - ) - - self.assertEqual( - result, - [ - (arrow.Arrow(2013, 5, 5, 12), arrow.Arrow(2013, 5, 5, 14)), - (arrow.Arrow(2013, 5, 5, 14), arrow.Arrow(2013, 5, 5, 16)), - (arrow.Arrow(2013, 5, 5, 16), arrow.Arrow(2013, 5, 5, 18)), - ], - ) - - -class ArrowSpanTests(Chai): - def setUp(self): - super(ArrowSpanTests, self).setUp() - - self.datetime = datetime(2013, 2, 15, 3, 41, 22, 8923) - self.arrow = arrow.Arrow.fromdatetime(self.datetime) - - def test_span_attribute(self): - - with self.assertRaises(AttributeError): - self.arrow.span("span") - - def test_span_year(self): - - floor, ceil = self.arrow.span("year") - - self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - ) - - def test_span_quarter(self): - - floor, ceil = self.arrow.span("quarter") - - self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) - self.assertEqual( - 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) - - self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) - self.assertEqual( - 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) - - self.assertEqual(floor, datetime(2013, 1, 1, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2014, 12, 31, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - ) - - def test_span_month(self): - - floor, ceil = self.arrow.span("month") - - self.assertEqual(floor, datetime(2013, 2, 1, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 2, 28, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - ) - - def test_span_week(self): - - floor, ceil = self.arrow.span("week") - - self.assertEqual(floor, datetime(2013, 2, 11, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 2, 17, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - ) - - def test_span_day(self): - - floor, ceil = self.arrow.span("day") - - self.assertEqual(floor, datetime(2013, 2, 15, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 2, 15, 23, 59, 59, 999999, tzinfo=tz.tzutc()) - ) - - def test_span_hour(self): - - floor, ceil = self.arrow.span("hour") - - self.assertEqual(floor, datetime(2013, 2, 15, 3, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 2, 15, 3, 59, 59, 999999, tzinfo=tz.tzutc()) - ) - - def test_span_minute(self): - - floor, ceil = self.arrow.span("minute") - - self.assertEqual(floor, datetime(2013, 2, 15, 3, 41, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 2, 15, 3, 41, 59, 999999, tzinfo=tz.tzutc()) - ) - - def test_span_second(self): - - floor, ceil = self.arrow.span("second") - - self.assertEqual(floor, datetime(2013, 2, 15, 3, 41, 22, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 2, 15, 3, 41, 22, 999999, tzinfo=tz.tzutc()) - ) - - def test_span_microsecond(self): - - floor, ceil = self.arrow.span("microsecond") - - self.assertEqual( - floor, datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) - ) - self.assertEqual( - ceil, datetime(2013, 2, 15, 3, 41, 22, 8923, tzinfo=tz.tzutc()) - ) - - def test_floor(self): - - floor, ceil = self.arrow.span("month") - - self.assertEqual(floor, self.arrow.floor("month")) - self.assertEqual(ceil, self.arrow.ceil("month")) - - def test_span_inclusive_inclusive(self): - - floor, ceil = self.arrow.span("hour", bounds="[]") - - self.assertEqual(floor, datetime(2013, 2, 15, 3, tzinfo=tz.tzutc())) - self.assertEqual(ceil, datetime(2013, 2, 15, 4, tzinfo=tz.tzutc())) - - def test_span_exclusive_inclusive(self): - - floor, ceil = self.arrow.span("hour", bounds="(]") - - self.assertEqual(floor, datetime(2013, 2, 15, 3, 0, 0, 1, tzinfo=tz.tzutc())) - self.assertEqual(ceil, datetime(2013, 2, 15, 4, tzinfo=tz.tzutc())) - - def test_span_exclusive_exclusive(self): - - floor, ceil = self.arrow.span("hour", bounds="()") - - self.assertEqual(floor, datetime(2013, 2, 15, 3, 0, 0, 1, tzinfo=tz.tzutc())) - self.assertEqual( - ceil, datetime(2013, 2, 15, 3, 59, 59, 999999, tzinfo=tz.tzutc()) - ) - - def test_bounds_are_validated(self): - - with self.assertRaises(AttributeError): - floor, ceil = self.arrow.span("hour", bounds="][") - - -class ArrowHumanizeTests(Chai): - def setUp(self): - super(ArrowHumanizeTests, self).setUp() - - self.datetime = datetime(2013, 1, 1) - self.now = arrow.Arrow.utcnow() - - def test_granularity(self): - - self.assertEqual(self.now.humanize(granularity="second"), "just now") - - later1 = self.now.shift(seconds=1) - self.assertEqual(self.now.humanize(later1, granularity="second"), "just now") - self.assertEqual(later1.humanize(self.now, granularity="second"), "just now") - self.assertEqual( - self.now.humanize(later1, granularity="minute"), "0 minutes ago" - ) - self.assertEqual( - later1.humanize(self.now, granularity="minute"), "in 0 minutes" - ) - - later100 = self.now.shift(seconds=100) - self.assertEqual( - self.now.humanize(later100, granularity="second"), "100 seconds ago" - ) - self.assertEqual( - later100.humanize(self.now, granularity="second"), "in 100 seconds" - ) - self.assertEqual( - self.now.humanize(later100, granularity="minute"), "a minute ago" - ) - self.assertEqual( - later100.humanize(self.now, granularity="minute"), "in a minute" - ) - self.assertEqual(self.now.humanize(later100, granularity="hour"), "0 hours ago") - self.assertEqual(later100.humanize(self.now, granularity="hour"), "in 0 hours") - - later4000 = self.now.shift(seconds=4000) - self.assertEqual( - self.now.humanize(later4000, granularity="minute"), "66 minutes ago" - ) - self.assertEqual( - later4000.humanize(self.now, granularity="minute"), "in 66 minutes" - ) - self.assertEqual( - self.now.humanize(later4000, granularity="hour"), "an hour ago" - ) - self.assertEqual(later4000.humanize(self.now, granularity="hour"), "in an hour") - self.assertEqual(self.now.humanize(later4000, granularity="day"), "0 days ago") - self.assertEqual(later4000.humanize(self.now, granularity="day"), "in 0 days") - - later105 = self.now.shift(seconds=10 ** 5) - self.assertEqual( - self.now.humanize(later105, granularity="hour"), "27 hours ago" - ) - self.assertEqual(later105.humanize(self.now, granularity="hour"), "in 27 hours") - self.assertEqual(self.now.humanize(later105, granularity="day"), "a day ago") - self.assertEqual(later105.humanize(self.now, granularity="day"), "in a day") - self.assertEqual(self.now.humanize(later105, granularity="week"), "0 weeks ago") - self.assertEqual(later105.humanize(self.now, granularity="week"), "in 0 weeks") - self.assertEqual( - self.now.humanize(later105, granularity="month"), "0 months ago" - ) - self.assertEqual( - later105.humanize(self.now, granularity="month"), "in 0 months" - ) - self.assertEqual( - self.now.humanize(later105, granularity=["month"]), "0 months ago" - ) - self.assertEqual( - later105.humanize(self.now, granularity=["month"]), "in 0 months" - ) - - later106 = self.now.shift(seconds=3 * 10 ** 6) - self.assertEqual(self.now.humanize(later106, granularity="day"), "34 days ago") - self.assertEqual(later106.humanize(self.now, granularity="day"), "in 34 days") - self.assertEqual(self.now.humanize(later106, granularity="week"), "4 weeks ago") - self.assertEqual(later106.humanize(self.now, granularity="week"), "in 4 weeks") - self.assertEqual( - self.now.humanize(later106, granularity="month"), "a month ago" - ) - self.assertEqual(later106.humanize(self.now, granularity="month"), "in a month") - self.assertEqual(self.now.humanize(later106, granularity="year"), "0 years ago") - self.assertEqual(later106.humanize(self.now, granularity="year"), "in 0 years") - - later506 = self.now.shift(seconds=50 * 10 ** 6) - self.assertEqual( - self.now.humanize(later506, granularity="week"), "82 weeks ago" - ) - self.assertEqual(later506.humanize(self.now, granularity="week"), "in 82 weeks") - self.assertEqual( - self.now.humanize(later506, granularity="month"), "18 months ago" - ) - self.assertEqual( - later506.humanize(self.now, granularity="month"), "in 18 months" - ) - self.assertEqual(self.now.humanize(later506, granularity="year"), "a year ago") - self.assertEqual(later506.humanize(self.now, granularity="year"), "in a year") - - later108 = self.now.shift(seconds=10 ** 8) - self.assertEqual(self.now.humanize(later108, granularity="year"), "3 years ago") - self.assertEqual(later108.humanize(self.now, granularity="year"), "in 3 years") - - later108onlydistance = self.now.shift(seconds=10 ** 8) - self.assertEqual( - self.now.humanize( - later108onlydistance, only_distance=True, granularity="year" - ), - "3 years", - ) - self.assertEqual( - later108onlydistance.humanize( - self.now, only_distance=True, granularity="year" - ), - "3 years", - ) - - with self.assertRaises(AttributeError): - self.now.humanize(later108, granularity="years") - - def test_multiple_granularity(self): - self.assertEqual(self.now.humanize(granularity="second"), "just now") - self.assertEqual(self.now.humanize(granularity=["second"]), "just now") - self.assertEqual( - self.now.humanize(granularity=["year", "month", "day", "hour", "second"]), - "in 0 years 0 months 0 days 0 hours and 0 seconds", - ) - - later4000 = self.now.shift(seconds=4000) - self.assertEqual( - later4000.humanize(self.now, granularity=["hour", "minute"]), - "in an hour and 6 minutes", - ) - self.assertEqual( - self.now.humanize(later4000, granularity=["hour", "minute"]), - "an hour and 6 minutes ago", - ) - self.assertEqual( - later4000.humanize( - self.now, granularity=["hour", "minute"], only_distance=True - ), - "an hour and 6 minutes", - ) - self.assertEqual( - later4000.humanize(self.now, granularity=["day", "hour", "minute"]), - "in 0 days an hour and 6 minutes", - ) - self.assertEqual( - self.now.humanize(later4000, granularity=["day", "hour", "minute"]), - "0 days an hour and 6 minutes ago", - ) - - later105 = self.now.shift(seconds=10 ** 5) - self.assertEqual( - self.now.humanize(later105, granularity=["hour", "day", "minute"]), - "a day 3 hours and 46 minutes ago", - ) - with self.assertRaises(AttributeError): - self.now.humanize(later105, granularity=["error", "second"]) - - later108onlydistance = self.now.shift(seconds=10 ** 8) - self.assertEqual( - self.now.humanize( - later108onlydistance, only_distance=True, granularity=["year"] - ), - "3 years", - ) - self.assertEqual( - self.now.humanize( - later108onlydistance, only_distance=True, granularity=["month", "week"] - ), - "37 months and 4 weeks", - ) - self.assertEqual( - self.now.humanize( - later108onlydistance, only_distance=True, granularity=["year", "second"] - ), - "3 years and 5327200 seconds", - ) - - one_min_one_sec_ago = self.now.shift(minutes=-1, seconds=-1) - self.assertEqual( - one_min_one_sec_ago.humanize(self.now, granularity=["minute", "second"]), - "a minute and a second ago", - ) - - one_min_two_secs_ago = self.now.shift(minutes=-1, seconds=-2) - self.assertEqual( - one_min_two_secs_ago.humanize(self.now, granularity=["minute", "second"]), - "a minute and 2 seconds ago", - ) - - def test_seconds(self): - - later = self.now.shift(seconds=10) - - # regression test for issue #727 - self.assertEqual(self.now.humanize(later), "10 seconds ago") - self.assertEqual(later.humanize(self.now), "in 10 seconds") - - self.assertEqual(self.now.humanize(later, only_distance=True), "10 seconds") - self.assertEqual(later.humanize(self.now, only_distance=True), "10 seconds") - - def test_minute(self): - - later = self.now.shift(minutes=1) - - self.assertEqual(self.now.humanize(later), "a minute ago") - self.assertEqual(later.humanize(self.now), "in a minute") - - self.assertEqual(self.now.humanize(later, only_distance=True), "a minute") - self.assertEqual(later.humanize(self.now, only_distance=True), "a minute") - - def test_minutes(self): - - later = self.now.shift(minutes=2) - - self.assertEqual(self.now.humanize(later), "2 minutes ago") - self.assertEqual(later.humanize(self.now), "in 2 minutes") - - self.assertEqual(self.now.humanize(later, only_distance=True), "2 minutes") - self.assertEqual(later.humanize(self.now, only_distance=True), "2 minutes") - - def test_hour(self): - - later = self.now.shift(hours=1) - - self.assertEqual(self.now.humanize(later), "an hour ago") - self.assertEqual(later.humanize(self.now), "in an hour") - - self.assertEqual(self.now.humanize(later, only_distance=True), "an hour") - self.assertEqual(later.humanize(self.now, only_distance=True), "an hour") - - def test_hours(self): - - later = self.now.shift(hours=2) - - self.assertEqual(self.now.humanize(later), "2 hours ago") - self.assertEqual(later.humanize(self.now), "in 2 hours") - - self.assertEqual(self.now.humanize(later, only_distance=True), "2 hours") - self.assertEqual(later.humanize(self.now, only_distance=True), "2 hours") - - def test_day(self): - - later = self.now.shift(days=1) - - self.assertEqual(self.now.humanize(later), "a day ago") - self.assertEqual(later.humanize(self.now), "in a day") - - # regression test for issue #697 - less_than_48_hours = self.now.shift( - days=1, hours=23, seconds=59, microseconds=999999 - ) - self.assertEqual(self.now.humanize(less_than_48_hours), "a day ago") - self.assertEqual(less_than_48_hours.humanize(self.now), "in a day") - - less_than_48_hours_date = less_than_48_hours._datetime.date() - with self.assertRaises(TypeError): - # humanize other argument does not take raw datetime.date objects - self.now.humanize(less_than_48_hours_date) - - # convert from date to arrow object - less_than_48_hours_date = arrow.Arrow.fromdate(less_than_48_hours_date) - self.assertEqual(self.now.humanize(less_than_48_hours_date), "a day ago") - self.assertEqual(less_than_48_hours_date.humanize(self.now), "in a day") - - self.assertEqual(self.now.humanize(later, only_distance=True), "a day") - self.assertEqual(later.humanize(self.now, only_distance=True), "a day") - - def test_days(self): - - later = self.now.shift(days=2) - - self.assertEqual(self.now.humanize(later), "2 days ago") - self.assertEqual(later.humanize(self.now), "in 2 days") - - self.assertEqual(self.now.humanize(later, only_distance=True), "2 days") - self.assertEqual(later.humanize(self.now, only_distance=True), "2 days") - - # Regression tests for humanize bug referenced in issue 541 - later = self.now.shift(days=3) - self.assertEqual(later.humanize(), "in 3 days") - - later = self.now.shift(days=3, seconds=1) - self.assertEqual(later.humanize(), "in 3 days") - - later = self.now.shift(days=4) - self.assertEqual(later.humanize(), "in 4 days") - - def test_week(self): - - later = self.now.shift(weeks=1) - - self.assertEqual(self.now.humanize(later), "a week ago") - self.assertEqual(later.humanize(self.now), "in a week") - - self.assertEqual(self.now.humanize(later, only_distance=True), "a week") - self.assertEqual(later.humanize(self.now, only_distance=True), "a week") - - def test_weeks(self): - - later = self.now.shift(weeks=2) - - self.assertEqual(self.now.humanize(later), "2 weeks ago") - self.assertEqual(later.humanize(self.now), "in 2 weeks") - - self.assertEqual(self.now.humanize(later, only_distance=True), "2 weeks") - self.assertEqual(later.humanize(self.now, only_distance=True), "2 weeks") - - def test_month(self): - - later = self.now.shift(months=1) - - self.assertEqual(self.now.humanize(later), "a month ago") - self.assertEqual(later.humanize(self.now), "in a month") - - self.assertEqual(self.now.humanize(later, only_distance=True), "a month") - self.assertEqual(later.humanize(self.now, only_distance=True), "a month") - - def test_months(self): - - later = self.now.shift(months=2) - earlier = self.now.shift(months=-2) - - self.assertEqual(earlier.humanize(self.now), "2 months ago") - self.assertEqual(later.humanize(self.now), "in 2 months") - - self.assertEqual(self.now.humanize(later, only_distance=True), "2 months") - self.assertEqual(later.humanize(self.now, only_distance=True), "2 months") - - def test_year(self): - - later = self.now.shift(years=1) - - self.assertEqual(self.now.humanize(later), "a year ago") - self.assertEqual(later.humanize(self.now), "in a year") - - self.assertEqual(self.now.humanize(later, only_distance=True), "a year") - self.assertEqual(later.humanize(self.now, only_distance=True), "a year") - - def test_years(self): - - later = self.now.shift(years=2) - - self.assertEqual(self.now.humanize(later), "2 years ago") - self.assertEqual(later.humanize(self.now), "in 2 years") - - self.assertEqual(self.now.humanize(later, only_distance=True), "2 years") - self.assertEqual(later.humanize(self.now, only_distance=True), "2 years") - - arw = arrow.Arrow(2014, 7, 2) - - result = arw.humanize(self.datetime) - - self.assertEqual(result, "in 2 years") - - def test_arrow(self): - - arw = arrow.Arrow.fromdatetime(self.datetime) - - result = arw.humanize(arrow.Arrow.fromdatetime(self.datetime)) - - self.assertEqual(result, "just now") - - def test_datetime_tzinfo(self): - - arw = arrow.Arrow.fromdatetime(self.datetime) - - result = arw.humanize(self.datetime.replace(tzinfo=tz.tzutc())) - - self.assertEqual(result, "just now") - - def test_other(self): - - arw = arrow.Arrow.fromdatetime(self.datetime) - - with self.assertRaises(TypeError): - arw.humanize(object()) - - def test_invalid_locale(self): - - arw = arrow.Arrow.fromdatetime(self.datetime) - - with self.assertRaises(ValueError): - arw.humanize(locale="klingon") - - def test_none(self): - - arw = arrow.Arrow.utcnow() - - result = arw.humanize() - - self.assertEqual(result, "just now") - - result = arw.humanize(None) - - self.assertEqual(result, "just now") - - def test_untranslated_granularity(self): - - arw = arrow.Arrow.utcnow() - later = arw.shift(weeks=1) - - # simulate an untranslated timeframe key - with patch.dict("arrow.locales.EnglishLocale.timeframes"): - del arrow.locales.EnglishLocale.timeframes["week"] - - with self.assertRaises(ValueError): - arw.humanize(later, granularity="week") - - -class ArrowHumanizeTestsWithLocale(Chai): - def setUp(self): - super(ArrowHumanizeTestsWithLocale, self).setUp() - - self.datetime = datetime(2013, 1, 1) - - def test_now(self): - - arw = arrow.Arrow(2013, 1, 1, 0, 0, 0) - - result = arw.humanize(self.datetime, locale="ru") - - self.assertEqual(result, "сейчас") - - def test_seconds(self): - arw = arrow.Arrow(2013, 1, 1, 0, 0, 44) - - result = arw.humanize(self.datetime, locale="ru") - - self.assertEqual(result, "через 44 несколько секунд") - - def test_years(self): - - arw = arrow.Arrow(2011, 7, 2) - - result = arw.humanize(self.datetime, locale="ru") - - self.assertEqual(result, "2 года назад") - - -class ArrowIsBetweenTests(Chai): - def test_start_before_end(self): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) - result = target.is_between(start, end) - self.assertFalse(result) - - def test_exclusive_exclusive_bounds(self): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 27)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 10)) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 36)) - result = target.is_between(start, end, "()") - self.assertTrue(result) - result = target.is_between(start, end) - self.assertTrue(result) - - def test_exclusive_exclusive_bounds_same_date(self): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - result = target.is_between(start, end, "()") - self.assertFalse(result) - - def test_inclusive_exclusive_bounds(self): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 6)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 4)) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 6)) - result = target.is_between(start, end, "[)") - self.assertFalse(result) - - def test_exclusive_inclusive_bounds(self): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - result = target.is_between(start, end, "(]") - self.assertTrue(result) - - def test_inclusive_inclusive_bounds_same_date(self): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - result = target.is_between(start, end, "[]") - self.assertTrue(result) - - def test_type_error_exception(self): - with self.assertRaises(TypeError): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - start = datetime(2013, 5, 5) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) - target.is_between(start, end) - - with self.assertRaises(TypeError): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) - end = datetime(2013, 5, 8) - target.is_between(start, end) - - with self.assertRaises(TypeError): - target.is_between(None, None) - - def test_attribute_error_exception(self): - target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) - end = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) - with self.assertRaises(AttributeError): - target.is_between(start, end, "][") - with self.assertRaises(AttributeError): - target.is_between(start, end, "") - with self.assertRaises(AttributeError): - target.is_between(start, end, "]") - with self.assertRaises(AttributeError): - target.is_between(start, end, "[") - with self.assertRaises(AttributeError): - target.is_between(start, end, "hello") - - -class ArrowUtilTests(Chai): - def test_get_datetime(self): - - get_datetime = arrow.Arrow._get_datetime - - arw = arrow.Arrow.utcnow() - dt = datetime.utcnow() - timestamp = time.time() - - self.assertEqual(get_datetime(arw), arw.datetime) - self.assertEqual(get_datetime(dt), dt) - self.assertEqual( - get_datetime(timestamp), arrow.Arrow.utcfromtimestamp(timestamp).datetime - ) - - with self.assertRaises(ValueError) as raise_ctx: - get_datetime("abc") - self.assertFalse("{}" in str(raise_ctx.exception)) - - def test_get_tzinfo(self): - - get_tzinfo = arrow.Arrow._get_tzinfo - - with self.assertRaises(ValueError) as raise_ctx: - get_tzinfo("abc") - self.assertFalse("{}" in str(raise_ctx.exception)) - - def test_get_iteration_params(self): - - self.assertEqual( - arrow.Arrow._get_iteration_params("end", None), ("end", sys.maxsize) - ) - self.assertEqual( - arrow.Arrow._get_iteration_params(None, 100), (arrow.Arrow.max, 100) - ) - self.assertEqual(arrow.Arrow._get_iteration_params(100, 120), (100, 120)) - - with self.assertRaises(ValueError): - arrow.Arrow._get_iteration_params(None, None) diff --git a/tests/factory_tests.py.bak b/tests/factory_tests.py.bak deleted file mode 100644 index abdd0d029..000000000 --- a/tests/factory_tests.py.bak +++ /dev/null @@ -1,383 +0,0 @@ -# -*- coding: utf-8 -*- -import time -from datetime import date, datetime - -import dateparser -from chai import Chai -from dateutil import tz - -from arrow import factory, util -from arrow.parser import ParserError - - -def assertDtEqual(dt1, dt2, within=10): - assertEqual(dt1.tzinfo, dt2.tzinfo) # noqa: F821 - assertTrue(abs(util.total_seconds(dt1 - dt2)) < within) # noqa: F821 - - -class GetTests(Chai): - def setUp(self): - super(GetTests, self).setUp() - - self.factory = factory.ArrowFactory() - - def test_no_args(self): - - assertDtEqual(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 - - self.assertEqual(no_arg, one_arg) - - def test_one_arg_none(self): - - assertDtEqual( - self.factory.get(None), datetime.utcnow().replace(tzinfo=tz.tzutc()) - ) - - def test_struct_time(self): - - assertDtEqual( - 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() - ) - - self.assertEqual(self.factory.get(int_timestamp), timestamp_dt) - - with self.assertRaises(ParserError): - self.factory.get(str(int_timestamp)) - - float_timestamp = time.time() - timestamp_dt = datetime.utcfromtimestamp(float_timestamp).replace( - tzinfo=tz.tzutc() - ) - - self.assertEqual(self.factory.get(float_timestamp), timestamp_dt) - - with self.assertRaises(ParserError): - self.factory.get(str(float_timestamp)) - - # Regression test for issue #216 - # Python 3 raises OverflowError, Python 2 raises ValueError - timestamp = 99999999999999999999999999.99999999999999999999999999 - with self.assertRaises((OverflowError, ValueError)): - self.factory.get(timestamp) - - def test_one_arg_timestamp_with_tzinfo(self): - - timestamp = time.time() - timestamp_dt = datetime.fromtimestamp(timestamp, tz=tz.tzutc()).astimezone( - tz.gettz("US/Pacific") - ) - timezone = tz.gettz("US/Pacific") - - assertDtEqual(self.factory.get(timestamp, tzinfo=timezone), timestamp_dt) - - def test_one_arg_arrow(self): - - arw = self.factory.utcnow() - result = self.factory.get(arw) - - self.assertEqual(arw, result) - - def test_one_arg_datetime(self): - - dt = datetime.utcnow().replace(tzinfo=tz.tzutc()) - - self.assertEqual(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()) - - self.assertEqual(self.factory.get(d), dt) - - def test_one_arg_tzinfo(self): - - self.expected = ( - datetime.utcnow() - .replace(tzinfo=tz.tzutc()) - .astimezone(tz.gettz("US/Pacific")) - ) - - assertDtEqual(self.factory.get(tz.gettz("US/Pacific")), self.expected) - - # regression test for issue #658 - def test_one_arg_dateparser_datetime(self): - expected = datetime(1990, 1, 1).replace(tzinfo=tz.tzutc()) - # dateparser outputs: datetime.datetime(1990, 1, 1, 0, 0, tzinfo=) - parsed_date = dateparser.parse("1990-01-01T00:00:00+00:00") - dt_output = self.factory.get(parsed_date)._datetime.replace(tzinfo=tz.tzutc()) - self.assertEqual(dt_output, expected) - - def test_kwarg_tzinfo(self): - - self.expected = ( - datetime.utcnow() - .replace(tzinfo=tz.tzutc()) - .astimezone(tz.gettz("US/Pacific")) - ) - - assertDtEqual(self.factory.get(tzinfo=tz.gettz("US/Pacific")), self.expected) - - def test_kwarg_tzinfo_string(self): - - self.expected = ( - datetime.utcnow() - .replace(tzinfo=tz.tzutc()) - .astimezone(tz.gettz("US/Pacific")) - ) - - assertDtEqual(self.factory.get(tzinfo="US/Pacific"), self.expected) - - with self.assertRaises(ParserError): - self.factory.get(tzinfo="US/PacificInvalidTzinfo") - - def test_one_arg_iso_str(self): - - dt = datetime.utcnow() - - assertDtEqual(self.factory.get(dt.isoformat()), dt.replace(tzinfo=tz.tzutc())) - - def test_one_arg_iso_calendar(self): - - pairs = [ - (datetime(2004, 1, 4), (2004, 1, 7)), - (datetime(2008, 12, 30), (2009, 1, 2)), - (datetime(2010, 1, 2), (2009, 53, 6)), - (datetime(2000, 2, 29), (2000, 9, 2)), - (datetime(2005, 1, 1), (2004, 53, 6)), - (datetime(2010, 1, 4), (2010, 1, 1)), - (datetime(2010, 1, 3), (2009, 53, 7)), - (datetime(2003, 12, 29), (2004, 1, 1)), - ] - - for pair in pairs: - dt, iso = pair - self.assertEqual(self.factory.get(iso), self.factory.get(dt)) - - with self.assertRaises(TypeError): - self.factory.get((2014, 7, 1, 4)) - - with self.assertRaises(TypeError): - self.factory.get((2014, 7)) - - with self.assertRaises(ValueError): - self.factory.get((2014, 70, 1)) - - with self.assertRaises(ValueError): - self.factory.get((2014, 7, 10)) - - def test_one_arg_other(self): - - with self.assertRaises(TypeError): - self.factory.get(object()) - - def test_one_arg_bool(self): - - with self.assertRaises(TypeError): - self.factory.get(False) - - with self.assertRaises(TypeError): - self.factory.get(True) - - def test_two_args_datetime_tzinfo(self): - - result = self.factory.get(datetime(2013, 1, 1), tz.gettz("US/Pacific")) - - self.assertEqual( - 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") - - self.assertEqual( - 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")) - - self.assertEqual( - 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") - - self.assertEqual( - result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) - ) - - def test_two_args_datetime_other(self): - - with self.assertRaises(TypeError): - self.factory.get(datetime.utcnow(), object()) - - def test_two_args_date_other(self): - - with self.assertRaises(TypeError): - self.factory.get(date.today(), object()) - - def test_two_args_str_str(self): - - result = self.factory.get("2013-01-01", "YYYY-MM-DD") - - self.assertEqual(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")) - - assertDtEqual( - result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) - ) - - 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") - - self.assertEqual( - 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"]) - - self.assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.tzutc())) - - def test_two_args_unicode_unicode(self): - - result = self.factory.get(u"2013-01-01", u"YYYY-MM-DD") - - self.assertEqual(result._datetime, datetime(2013, 1, 1, tzinfo=tz.tzutc())) - - def test_two_args_other(self): - - with self.assertRaises(TypeError): - self.factory.get(object(), object()) - - def test_three_args_with_tzinfo(self): - - timefmt = "YYYYMMDD" - d = "20150514" - - self.assertEqual( - self.factory.get(d, timefmt, tzinfo=tz.tzlocal()), - datetime(2015, 5, 14, tzinfo=tz.tzlocal()), - ) - - def test_three_args(self): - - self.assertEqual( - self.factory.get(2013, 1, 1), datetime(2013, 1, 1, tzinfo=tz.tzutc()) - ) - - def test_full_kwargs(self): - - self.assertEqual( - 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): - - self.assertEqual( - self.factory.get(year=2016, month=7, day=14), - datetime(2016, 7, 14, 0, 0, tzinfo=tz.tzutc()), - ) - - def test_tzinfo_string_kwargs(self): - result = self.factory.get("2019072807", "YYYYMMDDHH", tzinfo="UTC") - self.assertEqual( - result._datetime, datetime(2019, 7, 28, 7, 0, 0, 0, tzinfo=tz.tzutc()) - ) - - def test_insufficient_kwargs(self): - - with self.assertRaises(TypeError): - self.factory.get(year=2016) - - with self.assertRaises(TypeError): - self.factory.get(year=2016, month=7) - - def test_locale(self): - result = self.factory.get("2010", "YYYY", locale="ja") - self.assertEqual( - result._datetime, datetime(2010, 1, 1, 0, 0, 0, 0, tzinfo=tz.tzutc()) - ) - - # regression test for issue #701 - result = self.factory.get( - "Montag, 9. September 2019, 16:15-20:00", "dddd, D. MMMM YYYY", locale="de" - ) - self.assertEqual( - result._datetime, datetime(2019, 9, 9, 0, 0, 0, 0, tzinfo=tz.tzutc()) - ) - - def test_locale_kwarg_only(self): - res = self.factory.get(locale="ja") - self.assertEqual(res.tzinfo, tz.tzutc()) - - def test_locale_with_tzinfo(self): - res = self.factory.get(locale="ja", tzinfo=tz.gettz("Asia/Tokyo")) - self.assertEqual(res.tzinfo, tz.gettz("Asia/Tokyo")) - - -class UtcNowTests(Chai): - def setUp(self): - super(UtcNowTests, self).setUp() - - self.factory = factory.ArrowFactory() - - def test_utcnow(self): - - assertDtEqual( - self.factory.utcnow()._datetime, - datetime.utcnow().replace(tzinfo=tz.tzutc()), - ) - - -class NowTests(Chai): - def setUp(self): - super(NowTests, self).setUp() - - self.factory = factory.ArrowFactory() - - def test_no_tz(self): - - assertDtEqual(self.factory.now(), datetime.now(tz.tzlocal())) - - def test_tzinfo(self): - - assertDtEqual(self.factory.now(tz.gettz("EST")), datetime.now(tz.gettz("EST"))) - - def test_tz_str(self): - - assertDtEqual(self.factory.now("EST"), datetime.now(tz.gettz("EST"))) diff --git a/tests/formatter_tests.py.bak b/tests/formatter_tests.py.bak deleted file mode 100644 index 848ac260a..000000000 --- a/tests/formatter_tests.py.bak +++ /dev/null @@ -1,199 +0,0 @@ -# -*- coding: utf-8 -*- -import time -from datetime import datetime - -import pytz -from chai import Chai -from dateutil import tz as dateutil_tz - -from arrow import formatter - -from .utils import make_full_tz_list - - -class DateTimeFormatterFormatTokenTests(Chai): - def setUp(self): - super(DateTimeFormatterFormatTokenTests, self).setUp() - - self.formatter = formatter.DateTimeFormatter() - - def test_format(self): - - dt = datetime(2013, 2, 5, 12, 32, 51) - - result = self.formatter.format(dt, "MM-DD-YYYY hh:mm:ss a") - - self.assertEqual(result, "02-05-2013 12:32:51 pm") - - def test_year(self): - - dt = datetime(2013, 1, 1) - self.assertEqual(self.formatter._format_token(dt, "YYYY"), "2013") - self.assertEqual(self.formatter._format_token(dt, "YY"), "13") - - def test_month(self): - - dt = datetime(2013, 1, 1) - self.assertEqual(self.formatter._format_token(dt, "MMMM"), "January") - self.assertEqual(self.formatter._format_token(dt, "MMM"), "Jan") - self.assertEqual(self.formatter._format_token(dt, "MM"), "01") - self.assertEqual(self.formatter._format_token(dt, "M"), "1") - - def test_day(self): - - dt = datetime(2013, 2, 1) - self.assertEqual(self.formatter._format_token(dt, "DDDD"), "032") - self.assertEqual(self.formatter._format_token(dt, "DDD"), "32") - self.assertEqual(self.formatter._format_token(dt, "DD"), "01") - self.assertEqual(self.formatter._format_token(dt, "D"), "1") - self.assertEqual(self.formatter._format_token(dt, "Do"), "1st") - - self.assertEqual(self.formatter._format_token(dt, "dddd"), "Friday") - self.assertEqual(self.formatter._format_token(dt, "ddd"), "Fri") - self.assertEqual(self.formatter._format_token(dt, "d"), "5") - - def test_hour(self): - - dt = datetime(2013, 1, 1, 2) - self.assertEqual(self.formatter._format_token(dt, "HH"), "02") - self.assertEqual(self.formatter._format_token(dt, "H"), "2") - - dt = datetime(2013, 1, 1, 13) - self.assertEqual(self.formatter._format_token(dt, "HH"), "13") - self.assertEqual(self.formatter._format_token(dt, "H"), "13") - - dt = datetime(2013, 1, 1, 2) - self.assertEqual(self.formatter._format_token(dt, "hh"), "02") - self.assertEqual(self.formatter._format_token(dt, "h"), "2") - - dt = datetime(2013, 1, 1, 13) - self.assertEqual(self.formatter._format_token(dt, "hh"), "01") - self.assertEqual(self.formatter._format_token(dt, "h"), "1") - - # test that 12-hour time converts to '12' at midnight - dt = datetime(2013, 1, 1, 0) - self.assertEqual(self.formatter._format_token(dt, "hh"), "12") - self.assertEqual(self.formatter._format_token(dt, "h"), "12") - - def test_minute(self): - - dt = datetime(2013, 1, 1, 0, 1) - self.assertEqual(self.formatter._format_token(dt, "mm"), "01") - self.assertEqual(self.formatter._format_token(dt, "m"), "1") - - def test_second(self): - - dt = datetime(2013, 1, 1, 0, 0, 1) - self.assertEqual(self.formatter._format_token(dt, "ss"), "01") - self.assertEqual(self.formatter._format_token(dt, "s"), "1") - - def test_sub_second(self): - - dt = datetime(2013, 1, 1, 0, 0, 0, 123456) - self.assertEqual(self.formatter._format_token(dt, "SSSSSS"), "123456") - self.assertEqual(self.formatter._format_token(dt, "SSSSS"), "12345") - self.assertEqual(self.formatter._format_token(dt, "SSSS"), "1234") - self.assertEqual(self.formatter._format_token(dt, "SSS"), "123") - self.assertEqual(self.formatter._format_token(dt, "SS"), "12") - self.assertEqual(self.formatter._format_token(dt, "S"), "1") - - dt = datetime(2013, 1, 1, 0, 0, 0, 2000) - self.assertEqual(self.formatter._format_token(dt, "SSSSSS"), "002000") - self.assertEqual(self.formatter._format_token(dt, "SSSSS"), "00200") - self.assertEqual(self.formatter._format_token(dt, "SSSS"), "0020") - self.assertEqual(self.formatter._format_token(dt, "SSS"), "002") - self.assertEqual(self.formatter._format_token(dt, "SS"), "00") - self.assertEqual(self.formatter._format_token(dt, "S"), "0") - - def test_timestamp(self): - - timestamp = time.time() - dt = datetime.utcfromtimestamp(timestamp) - self.assertEqual(self.formatter._format_token(dt, "X"), str(int(timestamp))) - - def test_timezone(self): - - dt = datetime.utcnow().replace(tzinfo=dateutil_tz.gettz("US/Pacific")) - - result = self.formatter._format_token(dt, "ZZ") - self.assertTrue(result == "-07:00" or result == "-08:00") - - result = self.formatter._format_token(dt, "Z") - self.assertTrue(result == "-0700" or result == "-0800") - - def test_timezone_formatter(self): - - for full_name in make_full_tz_list(): - # 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_name) - ) - abbreviation = dt.tzname() - - result = self.formatter._format_token(dt, "ZZZ") - self.assertEqual(result, abbreviation) - - def test_am_pm(self): - - dt = datetime(2012, 1, 1, 11) - self.assertEqual(self.formatter._format_token(dt, "a"), "am") - self.assertEqual(self.formatter._format_token(dt, "A"), "AM") - - dt = datetime(2012, 1, 1, 13) - self.assertEqual(self.formatter._format_token(dt, "a"), "pm") - self.assertEqual(self.formatter._format_token(dt, "A"), "PM") - - def test_nonsense(self): - dt = datetime(2012, 1, 1, 11) - self.assertEqual(self.formatter._format_token(dt, None), None) - self.assertEqual(self.formatter._format_token(dt, "NONSENSE"), None) - - def test_escape(self): - - self.assertEqual( - self.formatter.format( - datetime(2015, 12, 10, 17, 9), "MMMM D, YYYY [at] h:mma" - ), - "December 10, 2015 at 5:09pm", - ) - - self.assertEqual( - self.formatter.format( - datetime(2015, 12, 10, 17, 9), "[MMMM] M D, YYYY [at] h:mma" - ), - "MMMM 12 10, 2015 at 5:09pm", - ) - - self.assertEqual( - self.formatter.format( - datetime(1990, 11, 25), - "[It happened on] MMMM Do [in the year] YYYY [a long time ago]", - ), - "It happened on November 25th in the year 1990 a long time ago", - ) - - self.assertEqual( - self.formatter.format( - datetime(1990, 11, 25), - "[It happened on] MMMM Do [in the][ year] YYYY [a long time ago]", - ), - "It happened on November 25th in the year 1990 a long time ago", - ) - - self.assertEqual( - self.formatter.format( - datetime(1, 1, 1), "[I'm][ entirely][ escaped,][ weee!]" - ), - "I'm entirely escaped, weee!", - ) - - # Special RegEx characters - self.assertEqual( - self.formatter.format( - datetime(2017, 12, 31, 2, 0), "MMM DD, YYYY |^${}().*+?<>-& h:mm A" - ), - "Dec 31, 2017 |^${}().*+?<>-& 2:00 AM", - ) - - # Escaping is atomic: brackets inside brackets are treated litterally - self.assertEqual(self.formatter.format(datetime(1, 1, 1), "[[[ ]]"), "[[ ]") diff --git a/tests/locales_tests.py.bak b/tests/locales_tests.py.bak deleted file mode 100644 index af205b435..000000000 --- a/tests/locales_tests.py.bak +++ /dev/null @@ -1,954 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from chai import Chai - -from arrow import arrow, locales - - -class LocaleValidationTests(Chai): - """Validate locales to ensure that translations are valid and complete""" - - def setUp(self): - super(LocaleValidationTests, self).setUp() - - self.locales = locales._locales - - def test_locale_validation(self): - - for _, locale_cls in self.locales.items(): - # 7 days + 1 spacer to allow for 1-indexing of months - self.assertEqual(len(locale_cls.day_names), 8) - self.assertTrue(locale_cls.day_names[0] == "") - # ensure that all string from index 1 onward are valid (not blank or None) - self.assertTrue(all(locale_cls.day_names[1:])) - - self.assertEqual(len(locale_cls.day_abbreviations), 8) - self.assertTrue(locale_cls.day_abbreviations[0] == "") - self.assertTrue(all(locale_cls.day_abbreviations[1:])) - - # 12 months + 1 spacer to allow for 1-indexing of months - self.assertEqual(len(locale_cls.month_names), 13) - self.assertTrue(locale_cls.month_names[0] == "") - self.assertTrue(all(locale_cls.month_names[1:])) - - self.assertEqual(len(locale_cls.month_abbreviations), 13) - self.assertTrue(locale_cls.month_abbreviations[0] == "") - self.assertTrue(all(locale_cls.month_abbreviations[1:])) - - self.assertTrue(len(locale_cls.names) > 0) - self.assertTrue(locale_cls.past is not None) - self.assertTrue(locale_cls.future is not None) - - -class ModuleTests(Chai): - def test_get_locale(self): - - mock_locales = self.mock(locales, "_locales") - mock_locale_cls = self.mock() - mock_locale = self.mock() - - self.expect(mock_locales.get).args("name").returns(mock_locale_cls) - self.expect(mock_locale_cls).returns(mock_locale) - - result = locales.get_locale("name") - - self.assertEqual(result, mock_locale) - - def test_locales(self): - - self.assertTrue(len(locales._locales) > 0) - - -class LocaleTests(Chai): - def setUp(self): - super(LocaleTests, self).setUp() - - self.locale = locales.EnglishLocale() - - def test_format_timeframe(self): - - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 hours") - self.assertEqual(self.locale._format_timeframe("hour", 0), "an hour") - - def test_format_relative_now(self): - - result = self.locale._format_relative("just now", "now", 0) - - self.assertEqual(result, "just now") - - def test_format_relative_past(self): - - result = self.locale._format_relative("an hour", "hour", 1) - - self.assertEqual(result, "in an hour") - - def test_format_relative_future(self): - - result = self.locale._format_relative("an hour", "hour", -1) - - self.assertEqual(result, "an hour ago") - - def test_ordinal_number(self): - self.assertEqual(self.locale.ordinal_number(0), "0th") - self.assertEqual(self.locale.ordinal_number(1), "1st") - self.assertEqual(self.locale.ordinal_number(2), "2nd") - self.assertEqual(self.locale.ordinal_number(3), "3rd") - self.assertEqual(self.locale.ordinal_number(4), "4th") - self.assertEqual(self.locale.ordinal_number(10), "10th") - self.assertEqual(self.locale.ordinal_number(11), "11th") - self.assertEqual(self.locale.ordinal_number(12), "12th") - self.assertEqual(self.locale.ordinal_number(13), "13th") - self.assertEqual(self.locale.ordinal_number(14), "14th") - self.assertEqual(self.locale.ordinal_number(21), "21st") - self.assertEqual(self.locale.ordinal_number(22), "22nd") - self.assertEqual(self.locale.ordinal_number(23), "23rd") - self.assertEqual(self.locale.ordinal_number(24), "24th") - - self.assertEqual(self.locale.ordinal_number(100), "100th") - self.assertEqual(self.locale.ordinal_number(101), "101st") - self.assertEqual(self.locale.ordinal_number(102), "102nd") - self.assertEqual(self.locale.ordinal_number(103), "103rd") - self.assertEqual(self.locale.ordinal_number(104), "104th") - self.assertEqual(self.locale.ordinal_number(110), "110th") - self.assertEqual(self.locale.ordinal_number(111), "111th") - self.assertEqual(self.locale.ordinal_number(112), "112th") - self.assertEqual(self.locale.ordinal_number(113), "113th") - self.assertEqual(self.locale.ordinal_number(114), "114th") - self.assertEqual(self.locale.ordinal_number(121), "121st") - self.assertEqual(self.locale.ordinal_number(122), "122nd") - self.assertEqual(self.locale.ordinal_number(123), "123rd") - self.assertEqual(self.locale.ordinal_number(124), "124th") - - def test_meridian_invalid_token(self): - self.assertEqual(self.locale.meridian(7, None), None) - self.assertEqual(self.locale.meridian(7, "B"), None) - self.assertEqual(self.locale.meridian(7, "NONSENSE"), None) - - -class EnglishLocaleTests(Chai): - def setUp(self): - super(EnglishLocaleTests, self).setUp() - - self.locale = locales.EnglishLocale() - - def test_describe(self): - self.assertEqual(self.locale.describe("now", only_distance=True), "instantly") - self.assertEqual(self.locale.describe("now", only_distance=False), "just now") - - -class ItalianLocalesTests(Chai): - def test_ordinal_number(self): - locale = locales.ItalianLocale() - - self.assertEqual(locale.ordinal_number(1), "1º") - - -class SpanishLocalesTests(Chai): - def test_ordinal_number(self): - locale = locales.SpanishLocale() - - self.assertEqual(locale.ordinal_number(1), "1º") - - def test_format_timeframe(self): - locale = locales.SpanishLocale() - self.assertEqual(locale._format_timeframe("now", 0), "ahora") - self.assertEqual(locale._format_timeframe("seconds", 1), "1 segundos") - self.assertEqual(locale._format_timeframe("seconds", 3), "3 segundos") - self.assertEqual(locale._format_timeframe("seconds", 30), "30 segundos") - self.assertEqual(locale._format_timeframe("minute", 1), "un minuto") - self.assertEqual(locale._format_timeframe("minutes", 4), "4 minutos") - self.assertEqual(locale._format_timeframe("minutes", 40), "40 minutos") - self.assertEqual(locale._format_timeframe("hour", 1), "una hora") - self.assertEqual(locale._format_timeframe("hours", 5), "5 horas") - self.assertEqual(locale._format_timeframe("hours", 23), "23 horas") - self.assertEqual(locale._format_timeframe("day", 1), "un día") - self.assertEqual(locale._format_timeframe("days", 6), "6 días") - self.assertEqual(locale._format_timeframe("days", 12), "12 días") - self.assertEqual(locale._format_timeframe("week", 1), "una semana") - self.assertEqual(locale._format_timeframe("weeks", 2), "2 semanas") - self.assertEqual(locale._format_timeframe("weeks", 3), "3 semanas") - self.assertEqual(locale._format_timeframe("month", 1), "un mes") - self.assertEqual(locale._format_timeframe("months", 7), "7 meses") - self.assertEqual(locale._format_timeframe("months", 11), "11 meses") - self.assertEqual(locale._format_timeframe("year", 1), "un año") - self.assertEqual(locale._format_timeframe("years", 8), "8 años") - self.assertEqual(locale._format_timeframe("years", 12), "12 años") - - self.assertEqual(locale._format_timeframe("now", 0), "ahora") - self.assertEqual(locale._format_timeframe("seconds", -1), "1 segundos") - self.assertEqual(locale._format_timeframe("seconds", -9), "9 segundos") - self.assertEqual(locale._format_timeframe("seconds", -12), "12 segundos") - self.assertEqual(locale._format_timeframe("minute", -1), "un minuto") - self.assertEqual(locale._format_timeframe("minutes", -2), "2 minutos") - self.assertEqual(locale._format_timeframe("minutes", -10), "10 minutos") - self.assertEqual(locale._format_timeframe("hour", -1), "una hora") - self.assertEqual(locale._format_timeframe("hours", -3), "3 horas") - self.assertEqual(locale._format_timeframe("hours", -11), "11 horas") - self.assertEqual(locale._format_timeframe("day", -1), "un día") - self.assertEqual(locale._format_timeframe("days", -2), "2 días") - self.assertEqual(locale._format_timeframe("days", -12), "12 días") - self.assertEqual(locale._format_timeframe("week", -1), "una semana") - self.assertEqual(locale._format_timeframe("weeks", -2), "2 semanas") - self.assertEqual(locale._format_timeframe("weeks", -3), "3 semanas") - self.assertEqual(locale._format_timeframe("month", -1), "un mes") - self.assertEqual(locale._format_timeframe("months", -3), "3 meses") - self.assertEqual(locale._format_timeframe("months", -13), "13 meses") - self.assertEqual(locale._format_timeframe("year", -1), "un año") - self.assertEqual(locale._format_timeframe("years", -4), "4 años") - self.assertEqual(locale._format_timeframe("years", -14), "14 años") - - -class FrenchLocalesTests(Chai): - def test_ordinal_number(self): - locale = locales.FrenchLocale() - - self.assertEqual(locale.ordinal_number(1), "1er") - self.assertEqual(locale.ordinal_number(2), "2e") - - -class RussianLocalesTests(Chai): - def test_plurals2(self): - - locale = locales.RussianLocale() - - self.assertEqual(locale._format_timeframe("hours", 0), "0 часов") - self.assertEqual(locale._format_timeframe("hours", 1), "1 час") - self.assertEqual(locale._format_timeframe("hours", 2), "2 часа") - self.assertEqual(locale._format_timeframe("hours", 4), "4 часа") - self.assertEqual(locale._format_timeframe("hours", 5), "5 часов") - self.assertEqual(locale._format_timeframe("hours", 21), "21 час") - self.assertEqual(locale._format_timeframe("hours", 22), "22 часа") - self.assertEqual(locale._format_timeframe("hours", 25), "25 часов") - - # feminine grammatical gender should be tested separately - self.assertEqual(locale._format_timeframe("minutes", 0), "0 минут") - self.assertEqual(locale._format_timeframe("minutes", 1), "1 минуту") - self.assertEqual(locale._format_timeframe("minutes", 2), "2 минуты") - self.assertEqual(locale._format_timeframe("minutes", 4), "4 минуты") - self.assertEqual(locale._format_timeframe("minutes", 5), "5 минут") - self.assertEqual(locale._format_timeframe("minutes", 21), "21 минуту") - self.assertEqual(locale._format_timeframe("minutes", 22), "22 минуты") - self.assertEqual(locale._format_timeframe("minutes", 25), "25 минут") - - -class PolishLocalesTests(Chai): - def test_plurals(self): - - locale = locales.PolishLocale() - - self.assertEqual(locale._format_timeframe("hours", 0), "0 godzin") - self.assertEqual(locale._format_timeframe("hours", 1), "1 godzin") - self.assertEqual(locale._format_timeframe("hours", 2), "2 godziny") - self.assertEqual(locale._format_timeframe("hours", 4), "4 godziny") - self.assertEqual(locale._format_timeframe("hours", 5), "5 godzin") - self.assertEqual(locale._format_timeframe("hours", 21), "21 godzin") - self.assertEqual(locale._format_timeframe("hours", 22), "22 godziny") - self.assertEqual(locale._format_timeframe("hours", 25), "25 godzin") - - -class IcelandicLocalesTests(Chai): - def setUp(self): - super(IcelandicLocalesTests, self).setUp() - - self.locale = locales.IcelandicLocale() - - def test_format_timeframe(self): - - self.assertEqual(self.locale._format_timeframe("minute", -1), "einni mínútu") - self.assertEqual(self.locale._format_timeframe("minute", 1), "eina mínútu") - - self.assertEqual(self.locale._format_timeframe("hours", -2), "2 tímum") - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 tíma") - self.assertEqual(self.locale._format_timeframe("now", 0), "rétt í þessu") - - -class MalayalamLocaleTests(Chai): - def setUp(self): - super(MalayalamLocaleTests, self).setUp() - - self.locale = locales.MalayalamLocale() - - def test_format_timeframe(self): - - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 മണിക്കൂർ") - self.assertEqual(self.locale._format_timeframe("hour", 0), "ഒരു മണിക്കൂർ") - - def test_format_relative_now(self): - - result = self.locale._format_relative("ഇപ്പോൾ", "now", 0) - - self.assertEqual(result, "ഇപ്പോൾ") - - def test_format_relative_past(self): - - result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", 1) - self.assertEqual(result, "ഒരു മണിക്കൂർ ശേഷം") - - def test_format_relative_future(self): - - result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", -1) - self.assertEqual(result, "ഒരു മണിക്കൂർ മുമ്പ്") - - -class HindiLocaleTests(Chai): - def setUp(self): - super(HindiLocaleTests, self).setUp() - - self.locale = locales.HindiLocale() - - def test_format_timeframe(self): - - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 घंटे") - self.assertEqual(self.locale._format_timeframe("hour", 0), "एक घंटा") - - def test_format_relative_now(self): - - result = self.locale._format_relative("अभी", "now", 0) - - self.assertEqual(result, "अभी") - - def test_format_relative_past(self): - - result = self.locale._format_relative("एक घंटा", "hour", 1) - self.assertEqual(result, "एक घंटा बाद") - - def test_format_relative_future(self): - - result = self.locale._format_relative("एक घंटा", "hour", -1) - self.assertEqual(result, "एक घंटा पहले") - - -class CzechLocaleTests(Chai): - def setUp(self): - super(CzechLocaleTests, self).setUp() - - self.locale = locales.CzechLocale() - - def test_format_timeframe(self): - - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 hodiny") - self.assertEqual(self.locale._format_timeframe("hours", 5), "5 hodin") - self.assertEqual(self.locale._format_timeframe("hour", 0), "0 hodin") - self.assertEqual(self.locale._format_timeframe("hours", -2), "2 hodinami") - self.assertEqual(self.locale._format_timeframe("hours", -5), "5 hodinami") - self.assertEqual(self.locale._format_timeframe("now", 0), "Teď") - - def test_format_relative_now(self): - - result = self.locale._format_relative("Teď", "now", 0) - self.assertEqual(result, "Teď") - - def test_format_relative_future(self): - - result = self.locale._format_relative("hodinu", "hour", 1) - self.assertEqual(result, "Za hodinu") - - def test_format_relative_past(self): - - result = self.locale._format_relative("hodinou", "hour", -1) - self.assertEqual(result, "Před hodinou") - - -class SlovakLocaleTests(Chai): - def setUp(self): - super(SlovakLocaleTests, self).setUp() - - self.locale = locales.SlovakLocale() - - def test_format_timeframe(self): - - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 hodiny") - self.assertEqual(self.locale._format_timeframe("hours", 5), "5 hodín") - self.assertEqual(self.locale._format_timeframe("hour", 0), "0 hodín") - self.assertEqual(self.locale._format_timeframe("hours", -2), "2 hodinami") - self.assertEqual(self.locale._format_timeframe("hours", -5), "5 hodinami") - self.assertEqual(self.locale._format_timeframe("now", 0), "Teraz") - - def test_format_relative_now(self): - - result = self.locale._format_relative("Teraz", "now", 0) - self.assertEqual(result, "Teraz") - - def test_format_relative_future(self): - - result = self.locale._format_relative("hodinu", "hour", 1) - self.assertEqual(result, "O hodinu") - - def test_format_relative_past(self): - - result = self.locale._format_relative("hodinou", "hour", -1) - self.assertEqual(result, "Pred hodinou") - - -class BulgarianLocaleTests(Chai): - def test_plurals2(self): - - locale = locales.BulgarianLocale() - - self.assertEqual(locale._format_timeframe("hours", 0), "0 часа") - self.assertEqual(locale._format_timeframe("hours", 1), "1 час") - self.assertEqual(locale._format_timeframe("hours", 2), "2 часа") - self.assertEqual(locale._format_timeframe("hours", 4), "4 часа") - self.assertEqual(locale._format_timeframe("hours", 5), "5 часа") - self.assertEqual(locale._format_timeframe("hours", 21), "21 час") - self.assertEqual(locale._format_timeframe("hours", 22), "22 часа") - self.assertEqual(locale._format_timeframe("hours", 25), "25 часа") - - # feminine grammatical gender should be tested separately - self.assertEqual(locale._format_timeframe("minutes", 0), "0 минути") - self.assertEqual(locale._format_timeframe("minutes", 1), "1 минута") - self.assertEqual(locale._format_timeframe("minutes", 2), "2 минути") - self.assertEqual(locale._format_timeframe("minutes", 4), "4 минути") - self.assertEqual(locale._format_timeframe("minutes", 5), "5 минути") - self.assertEqual(locale._format_timeframe("minutes", 21), "21 минута") - self.assertEqual(locale._format_timeframe("minutes", 22), "22 минути") - self.assertEqual(locale._format_timeframe("minutes", 25), "25 минути") - - -class MacedonianLocaleTests(Chai): - def test_plurals_mk(self): - - locale = locales.MacedonianLocale() - - # time - self.assertEqual(locale._format_relative("сега", "now", 0), "сега") - - # Hours - self.assertEqual(locale._format_timeframe("hours", 0), "0 саати") - self.assertEqual(locale._format_timeframe("hours", 1), "1 саат") - self.assertEqual(locale._format_timeframe("hours", 2), "2 саати") - self.assertEqual(locale._format_timeframe("hours", 4), "4 саати") - self.assertEqual(locale._format_timeframe("hours", 5), "5 саати") - self.assertEqual(locale._format_timeframe("hours", 21), "21 саат") - self.assertEqual(locale._format_timeframe("hours", 22), "22 саати") - self.assertEqual(locale._format_timeframe("hours", 25), "25 саати") - - # Minutes - self.assertEqual(locale._format_timeframe("minutes", 0), "0 минути") - self.assertEqual(locale._format_timeframe("minutes", 1), "1 минута") - self.assertEqual(locale._format_timeframe("minutes", 2), "2 минути") - self.assertEqual(locale._format_timeframe("minutes", 4), "4 минути") - self.assertEqual(locale._format_timeframe("minutes", 5), "5 минути") - self.assertEqual(locale._format_timeframe("minutes", 21), "21 минута") - self.assertEqual(locale._format_timeframe("minutes", 22), "22 минути") - self.assertEqual(locale._format_timeframe("minutes", 25), "25 минути") - - -class HebrewLocaleTests(Chai): - def test_couple_of_timeframe(self): - locale = locales.HebrewLocale() - - self.assertEqual(locale._format_timeframe("hours", 2), "שעתיים") - self.assertEqual(locale._format_timeframe("months", 2), "חודשיים") - self.assertEqual(locale._format_timeframe("days", 2), "יומיים") - self.assertEqual(locale._format_timeframe("years", 2), "שנתיים") - - self.assertEqual(locale._format_timeframe("hours", 3), "3 שעות") - self.assertEqual(locale._format_timeframe("months", 4), "4 חודשים") - self.assertEqual(locale._format_timeframe("days", 3), "3 ימים") - self.assertEqual(locale._format_timeframe("years", 5), "5 שנים") - - -class MarathiLocaleTests(Chai): - def setUp(self): - super(MarathiLocaleTests, self).setUp() - - self.locale = locales.MarathiLocale() - - def test_dateCoreFunctionality(self): - dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) - self.assertEqual(self.locale.month_name(dt.month), "एप्रिल") - self.assertEqual(self.locale.month_abbreviation(dt.month), "एप्रि") - self.assertEqual(self.locale.day_name(dt.isoweekday()), "शनिवार") - self.assertEqual(self.locale.day_abbreviation(dt.isoweekday()), "शनि") - - def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 तास") - self.assertEqual(self.locale._format_timeframe("hour", 0), "एक तास") - - def test_format_relative_now(self): - result = self.locale._format_relative("सद्य", "now", 0) - self.assertEqual(result, "सद्य") - - def test_format_relative_past(self): - result = self.locale._format_relative("एक तास", "hour", 1) - self.assertEqual(result, "एक तास नंतर") - - def test_format_relative_future(self): - result = self.locale._format_relative("एक तास", "hour", -1) - self.assertEqual(result, "एक तास आधी") - - # Not currently implemented - def test_ordinal_number(self): - self.assertEqual(self.locale.ordinal_number(1), "1") - - -class FinnishLocaleTests(Chai): - def setUp(self): - super(FinnishLocaleTests, self).setUp() - - self.locale = locales.FinnishLocale() - - def test_format_timeframe(self): - self.assertEqual( - self.locale._format_timeframe("hours", 2), ("2 tuntia", "2 tunnin") - ) - self.assertEqual(self.locale._format_timeframe("hour", 0), ("tunti", "tunnin")) - - def test_format_relative_now(self): - result = self.locale._format_relative(["juuri nyt", "juuri nyt"], "now", 0) - self.assertEqual(result, "juuri nyt") - - def test_format_relative_past(self): - result = self.locale._format_relative(["tunti", "tunnin"], "hour", 1) - self.assertEqual(result, "tunnin kuluttua") - - def test_format_relative_future(self): - result = self.locale._format_relative(["tunti", "tunnin"], "hour", -1) - self.assertEqual(result, "tunti sitten") - - def test_ordinal_number(self): - self.assertEqual(self.locale.ordinal_number(1), "1.") - - -class GermanLocaleTests(Chai): - def setUp(self): - super(GermanLocaleTests, self).setUp() - - self.locale = locales.GermanLocale() - - def test_ordinal_number(self): - self.assertEqual(self.locale.ordinal_number(1), "1.") - - def test_define(self): - self.assertEqual( - self.locale.describe("minute", only_distance=True), "eine Minute" - ) - self.assertEqual( - self.locale.describe("minute", only_distance=False), "in einer Minute" - ) - self.assertEqual( - self.locale.describe("hour", only_distance=True), "eine Stunde" - ) - self.assertEqual( - self.locale.describe("hour", only_distance=False), "in einer Stunde" - ) - self.assertEqual(self.locale.describe("day", only_distance=True), "ein Tag") - self.assertEqual( - self.locale.describe("day", only_distance=False), "in einem Tag" - ) - self.assertEqual(self.locale.describe("month", only_distance=True), "ein Monat") - self.assertEqual( - self.locale.describe("month", only_distance=False), "in einem Monat" - ) - self.assertEqual(self.locale.describe("year", only_distance=True), "ein Jahr") - self.assertEqual( - self.locale.describe("year", only_distance=False), "in einem Jahr" - ) - - -class HungarianLocaleTests(Chai): - def setUp(self): - super(HungarianLocaleTests, self).setUp() - - self.locale = locales.HungarianLocale() - - def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 óra") - self.assertEqual(self.locale._format_timeframe("hour", 0), "egy órával") - self.assertEqual(self.locale._format_timeframe("hours", -2), "2 órával") - self.assertEqual(self.locale._format_timeframe("now", 0), "éppen most") - - -class EsperantoLocaleTests(Chai): - def setUp(self): - super(EsperantoLocaleTests, self).setUp() - - self.locale = locales.EsperantoLocale() - - def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 horoj") - self.assertEqual(self.locale._format_timeframe("hour", 0), "un horo") - self.assertEqual(self.locale._format_timeframe("hours", -2), "2 horoj") - self.assertEqual(self.locale._format_timeframe("now", 0), "nun") - - def test_ordinal_number(self): - self.assertEqual(self.locale.ordinal_number(1), "1a") - - -class ThaiLocaleTests(Chai): - def setUp(self): - super(ThaiLocaleTests, self).setUp() - - self.locale = locales.ThaiLocale() - - def test_year_full(self): - self.assertEqual(self.locale.year_full(2015), "2558") - - def test_year_abbreviation(self): - self.assertEqual(self.locale.year_abbreviation(2015), "58") - - def test_format_relative_now(self): - result = self.locale._format_relative("ขณะนี้", "now", 0) - self.assertEqual(result, "ขณะนี้") - - def test_format_relative_past(self): - result = self.locale._format_relative("1 ชั่วโมง", "hour", 1) - self.assertEqual(result, "ในอีก 1 ชั่วโมง") - result = self.locale._format_relative("{0} ชั่วโมง", "hours", 2) - self.assertEqual(result, "ในอีก {0} ชั่วโมง") - result = self.locale._format_relative("ไม่กี่วินาที", "seconds", 42) - self.assertEqual(result, "ในอีกไม่กี่วินาที") - - def test_format_relative_future(self): - result = self.locale._format_relative("1 ชั่วโมง", "hour", -1) - self.assertEqual(result, "1 ชั่วโมง ที่ผ่านมา") - - -class BengaliLocaleTests(Chai): - def setUp(self): - super(BengaliLocaleTests, self).setUp() - - self.locale = locales.BengaliLocale() - - def test_ordinal_number(self): - result0 = self.locale._ordinal_number(0) - result1 = self.locale._ordinal_number(1) - result3 = self.locale._ordinal_number(3) - result4 = self.locale._ordinal_number(4) - result5 = self.locale._ordinal_number(5) - result6 = self.locale._ordinal_number(6) - result10 = self.locale._ordinal_number(10) - result11 = self.locale._ordinal_number(11) - result42 = self.locale._ordinal_number(42) - self.assertEqual(result0, "0তম") - self.assertEqual(result1, "1ম") - self.assertEqual(result3, "3য়") - self.assertEqual(result4, "4র্থ") - self.assertEqual(result5, "5ম") - self.assertEqual(result6, "6ষ্ঠ") - self.assertEqual(result10, "10ম") - self.assertEqual(result11, "11তম") - self.assertEqual(result42, "42তম") - self.assertEqual(self.locale._ordinal_number(-1), None) - - -class SwissLocaleTests(Chai): - def setUp(self): - super(SwissLocaleTests, self).setUp() - - self.locale = locales.SwissLocale() - - def test_ordinal_number(self): - dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) - - self.assertEqual(self.locale._format_timeframe("minute", 1), "einer Minute") - self.assertEqual(self.locale._format_timeframe("hour", 1), "einer Stunde") - self.assertEqual(self.locale.day_abbreviation(dt.isoweekday()), "Sa") - - -class RomanianLocaleTests(Chai): - def setUp(self): - super(RomanianLocaleTests, self).setUp() - - self.locale = locales.RomanianLocale() - - def test_timeframes(self): - - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 ore") - self.assertEqual(self.locale._format_timeframe("months", 2), "2 luni") - - self.assertEqual(self.locale._format_timeframe("days", 2), "2 zile") - self.assertEqual(self.locale._format_timeframe("years", 2), "2 ani") - - self.assertEqual(self.locale._format_timeframe("hours", 3), "3 ore") - self.assertEqual(self.locale._format_timeframe("months", 4), "4 luni") - self.assertEqual(self.locale._format_timeframe("days", 3), "3 zile") - self.assertEqual(self.locale._format_timeframe("years", 5), "5 ani") - - def test_relative_timeframes(self): - self.assertEqual(self.locale._format_relative("acum", "now", 0), "acum") - self.assertEqual( - self.locale._format_relative("o oră", "hour", 1), "peste o oră" - ) - self.assertEqual( - self.locale._format_relative("o oră", "hour", -1), "o oră în urmă" - ) - self.assertEqual( - self.locale._format_relative("un minut", "minute", 1), "peste un minut" - ) - self.assertEqual( - self.locale._format_relative("un minut", "minute", -1), "un minut în urmă" - ) - self.assertEqual( - self.locale._format_relative("câteva secunde", "seconds", -1), - "câteva secunde în urmă", - ) - self.assertEqual( - self.locale._format_relative("câteva secunde", "seconds", 1), - "peste câteva secunde", - ) - self.assertEqual( - self.locale._format_relative("o zi", "day", -1), "o zi în urmă" - ) - self.assertEqual(self.locale._format_relative("o zi", "day", 1), "peste o zi") - - -class ArabicLocalesTest(Chai): - def setUp(self): - super(ArabicLocalesTest, self).setUp() - - self.locale = locales.ArabicLocale() - - def test_timeframes(self): - - # single - self.assertEqual(self.locale._format_timeframe("minute", 1), "دقيقة") - self.assertEqual(self.locale._format_timeframe("hour", 1), "ساعة") - self.assertEqual(self.locale._format_timeframe("day", 1), "يوم") - self.assertEqual(self.locale._format_timeframe("month", 1), "شهر") - self.assertEqual(self.locale._format_timeframe("year", 1), "سنة") - - # double - self.assertEqual(self.locale._format_timeframe("minutes", 2), "دقيقتين") - self.assertEqual(self.locale._format_timeframe("hours", 2), "ساعتين") - self.assertEqual(self.locale._format_timeframe("days", 2), "يومين") - self.assertEqual(self.locale._format_timeframe("months", 2), "شهرين") - self.assertEqual(self.locale._format_timeframe("years", 2), "سنتين") - - # up to ten - self.assertEqual(self.locale._format_timeframe("minutes", 3), "3 دقائق") - self.assertEqual(self.locale._format_timeframe("hours", 4), "4 ساعات") - self.assertEqual(self.locale._format_timeframe("days", 5), "5 أيام") - self.assertEqual(self.locale._format_timeframe("months", 6), "6 أشهر") - self.assertEqual(self.locale._format_timeframe("years", 10), "10 سنوات") - - # more than ten - self.assertEqual(self.locale._format_timeframe("minutes", 11), "11 دقيقة") - self.assertEqual(self.locale._format_timeframe("hours", 19), "19 ساعة") - self.assertEqual(self.locale._format_timeframe("months", 24), "24 شهر") - self.assertEqual(self.locale._format_timeframe("days", 50), "50 يوم") - self.assertEqual(self.locale._format_timeframe("years", 115), "115 سنة") - - -class NepaliLocaleTests(Chai): - def setUp(self): - super(NepaliLocaleTests, self).setUp() - - self.locale = locales.NepaliLocale() - - def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("hours", 3), "3 घण्टा") - self.assertEqual(self.locale._format_timeframe("hour", 0), "एक घण्टा") - - def test_format_relative_now(self): - result = self.locale._format_relative("अहिले", "now", 0) - self.assertEqual(result, "अहिले") - - def test_format_relative_future(self): - result = self.locale._format_relative("एक घण्टा", "hour", 1) - self.assertEqual(result, "एक घण्टा पछी") - - def test_format_relative_past(self): - result = self.locale._format_relative("एक घण्टा", "hour", -1) - self.assertEqual(result, "एक घण्टा पहिले") - - -class IndonesianLocaleTests(Chai): - def setUp(self): - super(IndonesianLocaleTests, self).setUp() - - self.locale = locales.IndonesianLocale() - - def test_timeframes(self): - self.assertEqual(self.locale._format_timeframe("hours", 2), "2 jam") - self.assertEqual(self.locale._format_timeframe("months", 2), "2 bulan") - - self.assertEqual(self.locale._format_timeframe("days", 2), "2 hari") - self.assertEqual(self.locale._format_timeframe("years", 2), "2 tahun") - - self.assertEqual(self.locale._format_timeframe("hours", 3), "3 jam") - self.assertEqual(self.locale._format_timeframe("months", 4), "4 bulan") - self.assertEqual(self.locale._format_timeframe("days", 3), "3 hari") - self.assertEqual(self.locale._format_timeframe("years", 5), "5 tahun") - - def test_format_relative_now(self): - self.assertEqual( - self.locale._format_relative("baru saja", "now", 0), "baru saja" - ) - - def test_format_relative_past(self): - self.assertEqual( - self.locale._format_relative("1 jam", "hour", 1), "dalam 1 jam" - ) - self.assertEqual( - self.locale._format_relative("1 detik", "seconds", 1), "dalam 1 detik" - ) - - def test_format_relative_future(self): - self.assertEqual( - self.locale._format_relative("1 jam", "hour", -1), "1 jam yang lalu" - ) - - -class TagalogLocaleTests(Chai): - def setUp(self): - super(TagalogLocaleTests, self).setUp() - - self.locale = locales.TagalogLocale() - - def test_format_timeframe(self): - - self.assertEqual(self.locale._format_timeframe("minute", 1), "isang minuto") - self.assertEqual(self.locale._format_timeframe("hour", 1), "isang oras") - self.assertEqual(self.locale._format_timeframe("month", 1), "isang buwan") - self.assertEqual(self.locale._format_timeframe("year", 1), "isang taon") - - self.assertEqual(self.locale._format_timeframe("seconds", 2), "2 segundo") - self.assertEqual(self.locale._format_timeframe("minutes", 3), "3 minuto") - self.assertEqual(self.locale._format_timeframe("hours", 4), "4 oras") - self.assertEqual(self.locale._format_timeframe("months", 5), "5 buwan") - self.assertEqual(self.locale._format_timeframe("years", 6), "6 taon") - - def test_format_relative_now(self): - self.assertEqual( - self.locale._format_relative("ngayon lang", "now", 0), "ngayon lang" - ) - - def test_format_relative_past(self): - self.assertEqual( - self.locale._format_relative("2 oras", "hour", 2), "2 oras mula ngayon" - ) - - def test_format_relative_future(self): - self.assertEqual( - self.locale._format_relative("3 oras", "hour", -3), "nakaraang 3 oras" - ) - - def test_ordinal_number(self): - self.assertEqual(self.locale.ordinal_number(0), "ika-0") - self.assertEqual(self.locale.ordinal_number(1), "ika-1") - self.assertEqual(self.locale.ordinal_number(2), "ika-2") - self.assertEqual(self.locale.ordinal_number(3), "ika-3") - self.assertEqual(self.locale.ordinal_number(10), "ika-10") - self.assertEqual(self.locale.ordinal_number(23), "ika-23") - self.assertEqual(self.locale.ordinal_number(100), "ika-100") - self.assertEqual(self.locale.ordinal_number(103), "ika-103") - self.assertEqual(self.locale.ordinal_number(114), "ika-114") - - -class EstonianLocaleTests(Chai): - def setUp(self): - super(EstonianLocaleTests, self).setUp() - - self.locale = locales.EstonianLocale() - - def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("now", 0), "just nüüd") - self.assertEqual(self.locale._format_timeframe("second", 1), "ühe sekundi") - self.assertEqual(self.locale._format_timeframe("seconds", 3), "3 sekundi") - self.assertEqual(self.locale._format_timeframe("seconds", 30), "30 sekundi") - self.assertEqual(self.locale._format_timeframe("minute", 1), "ühe minuti") - self.assertEqual(self.locale._format_timeframe("minutes", 4), "4 minuti") - self.assertEqual(self.locale._format_timeframe("minutes", 40), "40 minuti") - self.assertEqual(self.locale._format_timeframe("hour", 1), "tunni aja") - self.assertEqual(self.locale._format_timeframe("hours", 5), "5 tunni") - self.assertEqual(self.locale._format_timeframe("hours", 23), "23 tunni") - self.assertEqual(self.locale._format_timeframe("day", 1), "ühe päeva") - self.assertEqual(self.locale._format_timeframe("days", 6), "6 päeva") - self.assertEqual(self.locale._format_timeframe("days", 12), "12 päeva") - self.assertEqual(self.locale._format_timeframe("month", 1), "ühe kuu") - self.assertEqual(self.locale._format_timeframe("months", 7), "7 kuu") - self.assertEqual(self.locale._format_timeframe("months", 11), "11 kuu") - self.assertEqual(self.locale._format_timeframe("year", 1), "ühe aasta") - self.assertEqual(self.locale._format_timeframe("years", 8), "8 aasta") - self.assertEqual(self.locale._format_timeframe("years", 12), "12 aasta") - - self.assertEqual(self.locale._format_timeframe("now", 0), "just nüüd") - self.assertEqual(self.locale._format_timeframe("second", -1), "üks sekund") - self.assertEqual(self.locale._format_timeframe("seconds", -9), "9 sekundit") - self.assertEqual(self.locale._format_timeframe("seconds", -12), "12 sekundit") - self.assertEqual(self.locale._format_timeframe("minute", -1), "üks minut") - self.assertEqual(self.locale._format_timeframe("minutes", -2), "2 minutit") - self.assertEqual(self.locale._format_timeframe("minutes", -10), "10 minutit") - self.assertEqual(self.locale._format_timeframe("hour", -1), "tund aega") - self.assertEqual(self.locale._format_timeframe("hours", -3), "3 tundi") - self.assertEqual(self.locale._format_timeframe("hours", -11), "11 tundi") - self.assertEqual(self.locale._format_timeframe("day", -1), "üks päev") - self.assertEqual(self.locale._format_timeframe("days", -2), "2 päeva") - self.assertEqual(self.locale._format_timeframe("days", -12), "12 päeva") - self.assertEqual(self.locale._format_timeframe("month", -1), "üks kuu") - self.assertEqual(self.locale._format_timeframe("months", -3), "3 kuud") - self.assertEqual(self.locale._format_timeframe("months", -13), "13 kuud") - self.assertEqual(self.locale._format_timeframe("year", -1), "üks aasta") - self.assertEqual(self.locale._format_timeframe("years", -4), "4 aastat") - self.assertEqual(self.locale._format_timeframe("years", -14), "14 aastat") - - -class PortugueseLocaleTests(Chai): - def setUp(self): - super(PortugueseLocaleTests, self).setUp() - - self.locale = locales.PortugueseLocale() - - def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("now", 0), "agora") - self.assertEqual(self.locale._format_timeframe("second", 1), "um segundo") - self.assertEqual(self.locale._format_timeframe("seconds", 30), "30 segundos") - self.assertEqual(self.locale._format_timeframe("minute", 1), "um minuto") - self.assertEqual(self.locale._format_timeframe("minutes", 40), "40 minutos") - self.assertEqual(self.locale._format_timeframe("hour", 1), "uma hora") - self.assertEqual(self.locale._format_timeframe("hours", 23), "23 horas") - self.assertEqual(self.locale._format_timeframe("day", 1), "um dia") - self.assertEqual(self.locale._format_timeframe("days", 12), "12 dias") - self.assertEqual(self.locale._format_timeframe("month", 1), "um mês") - self.assertEqual(self.locale._format_timeframe("months", 11), "11 meses") - self.assertEqual(self.locale._format_timeframe("year", 1), "um ano") - self.assertEqual(self.locale._format_timeframe("years", 12), "12 anos") - - -class BrazilianLocaleTests(Chai): - def setUp(self): - super(BrazilianLocaleTests, self).setUp() - - self.locale = locales.BrazilianPortugueseLocale() - - def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("now", 0), "agora") - self.assertEqual(self.locale._format_timeframe("second", 1), "um segundo") - self.assertEqual(self.locale._format_timeframe("seconds", 30), "30 segundos") - self.assertEqual(self.locale._format_timeframe("minute", 1), "um minuto") - self.assertEqual(self.locale._format_timeframe("minutes", 40), "40 minutos") - self.assertEqual(self.locale._format_timeframe("hour", 1), "uma hora") - self.assertEqual(self.locale._format_timeframe("hours", 23), "23 horas") - self.assertEqual(self.locale._format_timeframe("day", 1), "um dia") - self.assertEqual(self.locale._format_timeframe("days", 12), "12 dias") - self.assertEqual(self.locale._format_timeframe("month", 1), "um mês") - self.assertEqual(self.locale._format_timeframe("months", 11), "11 meses") - self.assertEqual(self.locale._format_timeframe("year", 1), "um ano") - self.assertEqual(self.locale._format_timeframe("years", 12), "12 anos") - - -class HongKongLocaleTests(Chai): - def setUp(self): - super(HongKongLocaleTests, self).setUp() - - self.locale = locales.HongKongLocale() - - def test_format_timeframe(self): - self.assertEqual(self.locale._format_timeframe("now", 0), "剛才") - self.assertEqual(self.locale._format_timeframe("second", 1), "1秒") - self.assertEqual(self.locale._format_timeframe("seconds", 30), "30秒") - self.assertEqual(self.locale._format_timeframe("minute", 1), "1分鐘") - self.assertEqual(self.locale._format_timeframe("minutes", 40), "40分鐘") - self.assertEqual(self.locale._format_timeframe("hour", 1), "1小時") - self.assertEqual(self.locale._format_timeframe("hours", 23), "23小時") - self.assertEqual(self.locale._format_timeframe("day", 1), "1天") - self.assertEqual(self.locale._format_timeframe("days", 12), "12天") - self.assertEqual(self.locale._format_timeframe("week", 1), "1星期") - self.assertEqual(self.locale._format_timeframe("weeks", 38), "38星期") - self.assertEqual(self.locale._format_timeframe("month", 1), "1個月") - self.assertEqual(self.locale._format_timeframe("months", 11), "11個月") - self.assertEqual(self.locale._format_timeframe("year", 1), "1年") - self.assertEqual(self.locale._format_timeframe("years", 12), "12年") diff --git a/tests/parser_tests.py b/tests/parser_tests.py deleted file mode 100644 index c72da044b..000000000 --- a/tests/parser_tests.py +++ /dev/null @@ -1,1629 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -import calendar -import os -import time -from datetime import datetime - -from chai import Chai -from dateutil import tz - -from arrow import parser -from arrow.constants import MAX_TIMESTAMP_US -from arrow.parser import DateTimeParser, ParserError, ParserMatchError - -from .utils import make_full_tz_list - - -class DateTimeParserTests(Chai): - def setUp(self): - super(DateTimeParserTests, self).setUp() - - self.parser = parser.DateTimeParser() - - def test_parse_multiformat(self): - - mock_datetime = self.mock() - - self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserMatchError) - self.expect(self.parser.parse).args("str", "fmt_b").returns(mock_datetime) - - result = self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) - - self.assertEqual(result, mock_datetime) - - def test_parse_multiformat_all_fail(self): - - self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserMatchError) - self.expect(self.parser.parse).args("str", "fmt_b").raises(ParserMatchError) - - with self.assertRaises(ParserError): - self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) - - def test_parse_multiformat_unself_expected_fail(self): - class UnselfExpectedError(Exception): - pass - - self.expect(self.parser.parse).args("str", "fmt_a").raises(UnselfExpectedError) - - with self.assertRaises(UnselfExpectedError): - self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) - - def test_parse_token_nonsense(self): - parts = {} - self.parser._parse_token("NONSENSE", "1900", parts) - self.assertEqual(parts, {}) - - def test_parse_token_invalid_meridians(self): - parts = {} - self.parser._parse_token("A", "a..m", parts) - self.assertEqual(parts, {}) - self.parser._parse_token("a", "p..m", parts) - self.assertEqual(parts, {}) - - def test_parser_no_caching(self): - - self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( - 100 - ) - self.parser = parser.DateTimeParser(cache_size=0) - for _ in range(100): - self.parser._generate_pattern_re("fmt_a") - - def test_parser_1_line_caching(self): - - self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( - 1 - ) - self.parser = parser.DateTimeParser(cache_size=1) - for _ in range(100): - self.parser._generate_pattern_re("fmt_a") - - self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_b").times( - 1 - ) - for _ in range(100): - self.parser._generate_pattern_re("fmt_a") - self.parser._generate_pattern_re("fmt_b") - - self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( - 1 - ) - for _ in range(100): - self.parser._generate_pattern_re("fmt_a") - - def test_parser_multiple_line_caching(self): - - self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( - 1 - ) - self.parser = parser.DateTimeParser(cache_size=2) - for _ in range(100): - self.parser._generate_pattern_re("fmt_a") - - self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_b").times( - 1 - ) - for _ in range(100): - self.parser._generate_pattern_re("fmt_a") - self.parser._generate_pattern_re("fmt_b") - - self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( - 0 - ) - for _ in range(100): - self.parser._generate_pattern_re("fmt_a") - - def test_YY_and_YYYY_format_list(self): - - self.assertEqual( - self.parser.parse("15/01/19", ["DD/MM/YY", "DD/MM/YYYY"]), - datetime(2019, 1, 15), - ) - - # Regression test for issue #580 - self.assertEqual( - self.parser.parse("15/01/2019", ["DD/MM/YY", "DD/MM/YYYY"]), - datetime(2019, 1, 15), - ) - - self.assertEqual( - 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 - self.assertEqual( - self.parser.parse( - "15 Jul 2000", - ["MM/DD/YYYY", "YYYY-MM-DD", "X", "DD-MMMM-YYYY", "D MMM YYYY"], - ), - datetime(2000, 7, 15), - ) - - with self.assertRaises(ParserError): - self.parser.parse("15 Jul", "X") - - -class DateTimeParserParseTests(Chai): - def setUp(self): - super(DateTimeParserParseTests, self).setUp() - - self.parser = parser.DateTimeParser() - - def test_parse_list(self): - - self.expect(self.parser._parse_multiformat).args( - "str", ["fmt_a", "fmt_b"] - ).returns("result") - - result = self.parser.parse("str", ["fmt_a", "fmt_b"]) - - self.assertEqual(result, "result") - - def test_parse_unrecognized_token(self): - - mock_input_re_map = self.mock(self.parser, "_input_re_map") - - self.expect(mock_input_re_map.__getitem__).args("YYYY").raises(KeyError) - - with self.assertRaises(parser.ParserError): - self.parser.parse("2013-01-01", "YYYY-MM-DD") - - def test_parse_parse_no_match(self): - - with self.assertRaises(ParserError): - self.parser.parse("01-01", "YYYY-MM-DD") - - def test_parse_separators(self): - - with self.assertRaises(ParserError): - self.parser.parse("1403549231", "YYYY-MM-DD") - - def test_parse_numbers(self): - - self.expected = datetime(2012, 1, 1, 12, 5, 10) - self.assertEqual( - self.parser.parse("2012-01-01 12:05:10", "YYYY-MM-DD HH:mm:ss"), - self.expected, - ) - - def test_parse_year_two_digit(self): - - self.expected = datetime(1979, 1, 1, 12, 5, 10) - self.assertEqual( - 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() - int_timestamp = int(time.time()) - self.expected = datetime.fromtimestamp(int_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:d}".format(int_timestamp), "X"), self.expected - ) - - float_timestamp = time.time() - self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:f}".format(float_timestamp), "X"), self.expected - ) - - # test handling of ns timestamp (arrow will round to 6 digits regardless) - self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:f}123".format(float_timestamp), "X"), self.expected - ) - - # test ps timestamp (arrow will round to 6 digits regardless) - self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:f}123456".format(float_timestamp), "X"), self.expected - ) - - # NOTE: negative timestamps cannot be handled by datetime on Window - # Must use timedelta to handle them. ref: https://stackoverflow.com/questions/36179914 - if os.name != "nt": - # regression test for issue #662 - negative_int_timestamp = -int_timestamp - self.expected = datetime.fromtimestamp(negative_int_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:d}".format(negative_int_timestamp), "X"), - self.expected, - ) - - negative_float_timestamp = -float_timestamp - self.expected = datetime.fromtimestamp(negative_float_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:f}".format(negative_float_timestamp), "X"), - self.expected, - ) - - # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will - # break cases like "15 Jul 2000" and a format list (see issue #447) - with self.assertRaises(ParserError): - natural_lang_string = "Meet me at {} at the restaurant.".format( - float_timestamp - ) - self.parser.parse(natural_lang_string, "X") - - with self.assertRaises(ParserError): - self.parser.parse("1565982019.", "X") - - with self.assertRaises(ParserError): - self.parser.parse(".1565982019", "X") - - def test_parse_expanded_timestamp(self): - # test expanded timestamps that include milliseconds - # and microseconds as multiples rather than decimals - # requested in issue #357 - - tz_utc = tz.tzutc() - timestamp = 1569982581.413132 - timestamp_milli = int(round(timestamp * 1000)) - timestamp_micro = int(round(timestamp * 1000000)) - - # "x" token should parse integer timestamps below MAX_TIMESTAMP normally - self.expected = datetime.fromtimestamp(int(timestamp), tz=tz_utc) - self.assertEqual( - self.parser.parse("{:d}".format(int(timestamp)), "x"), self.expected - ) - - self.expected = datetime.fromtimestamp(round(timestamp, 3), tz=tz_utc) - self.assertEqual( - self.parser.parse("{:d}".format(timestamp_milli), "x"), self.expected - ) - - self.expected = datetime.fromtimestamp(timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:d}".format(timestamp_micro), "x"), self.expected - ) - - # anything above max µs timestamp should fail - with self.assertRaises(ValueError): - self.parser.parse("{:d}".format(int(MAX_TIMESTAMP_US) + 1), "x") - - # floats are not allowed with the "x" token - with self.assertRaises(ParserMatchError): - self.parser.parse("{:f}".format(timestamp), "x") - - def test_parse_names(self): - - self.expected = datetime(2012, 1, 1) - - self.assertEqual( - self.parser.parse("January 1, 2012", "MMMM D, YYYY"), self.expected - ) - self.assertEqual(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) - self.assertEqual(self.parser.parse("1 pm", "H a"), self.expected) - self.assertEqual(self.parser.parse("1 pm", "h a"), self.expected) - - self.expected = datetime(1, 1, 1, 1, 0, 0) - self.assertEqual(self.parser.parse("1 am", "H A"), self.expected) - self.assertEqual(self.parser.parse("1 am", "h A"), self.expected) - - self.expected = datetime(1, 1, 1, 0, 0, 0) - self.assertEqual(self.parser.parse("12 am", "H A"), self.expected) - self.assertEqual(self.parser.parse("12 am", "h A"), self.expected) - - self.expected = datetime(1, 1, 1, 12, 0, 0) - self.assertEqual(self.parser.parse("12 pm", "H A"), self.expected) - self.assertEqual(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") - self.assertEqual(parsed, self.expected) - - def test_parse_tz_zz(self): - - self.expected = datetime(2013, 1, 1, tzinfo=tz.tzoffset(None, -7 * 3600)) - self.assertEqual( - self.parser.parse("2013-01-01 -07:00", "YYYY-MM-DD ZZ"), self.expected - ) - - def test_parse_tz_name_zzz(self): - for tz_name in make_full_tz_list(): - self.expected = datetime(2013, 1, 1, tzinfo=tz.gettz(tz_name)) - self.assertEqual( - self.parser.parse("2013-01-01 %s" % tz_name, "YYYY-MM-DD ZZZ"), - self.expected, - ) - - # note that offsets are not timezones - with self.assertRaises(ParserError): - self.parser.parse("2013-01-01 12:30:45.9+1000", "YYYY-MM-DDZZZ") - - with self.assertRaises(ParserError): - self.parser.parse("2013-01-01 12:30:45.9+10:00", "YYYY-MM-DDZZZ") - - with self.assertRaises(ParserError): - self.parser.parse("2013-01-01 12:30:45.9-10", "YYYY-MM-DDZZZ") - - def test_parse_subsecond(self): - # TODO: make both test_parse_subsecond functions in Parse and ParseISO - # tests use the same expected objects (use pytest fixtures) - self.expected = datetime(2013, 1, 1, 12, 30, 45, 900000) - self.assertEqual( - self.parser.parse("2013-01-01 12:30:45.9", "YYYY-MM-DD HH:mm:ss.S"), - self.expected, - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 980000) - self.assertEqual( - self.parser.parse("2013-01-01 12:30:45.98", "YYYY-MM-DD HH:mm:ss.SS"), - self.expected, - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987000) - self.assertEqual( - self.parser.parse("2013-01-01 12:30:45.987", "YYYY-MM-DD HH:mm:ss.SSS"), - self.expected, - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987600) - self.assertEqual( - self.parser.parse("2013-01-01 12:30:45.9876", "YYYY-MM-DD HH:mm:ss.SSSS"), - self.expected, - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987650) - self.assertEqual( - self.parser.parse("2013-01-01 12:30:45.98765", "YYYY-MM-DD HH:mm:ss.SSSSS"), - self.expected, - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - self.assertEqual( - self.parser.parse( - "2013-01-01 12:30:45.987654", "YYYY-MM-DD HH:mm:ss.SSSSSS" - ), - self.expected, - ) - - def test_parse_subsecond_rounding(self): - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - datetime_format = "YYYY-MM-DD HH:mm:ss.S" - - # round up - string = "2013-01-01 12:30:45.9876539" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - # round down - string = "2013-01-01 12:30:45.98765432" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - # round half-up - string = "2013-01-01 12:30:45.987653521" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - # round half-down - string = "2013-01-01 12:30:45.9876545210" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - # overflow (zero out the subseconds and increment the seconds) - # regression tests for issue #636 - def test_parse_subsecond_rounding_overflow(self): - datetime_format = "YYYY-MM-DD HH:mm:ss.S" - - self.expected = datetime(2013, 1, 1, 12, 30, 46) - string = "2013-01-01 12:30:45.9999995" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - self.expected = datetime(2013, 1, 1, 12, 31, 0) - string = "2013-01-01 12:30:59.9999999" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - self.expected = datetime(2013, 1, 2, 0, 0, 0) - string = "2013-01-01 23:59:59.9999999" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - # 6 digits should remain unrounded - self.expected = datetime(2013, 1, 1, 12, 30, 45, 999999) - string = "2013-01-01 12:30:45.999999" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - # Regression tests for issue #560 - def test_parse_long_year(self): - with self.assertRaises(ParserError): - self.parser.parse("09 January 123456789101112", "DD MMMM YYYY") - - with self.assertRaises(ParserError): - self.parser.parse("123456789101112 09 January", "YYYY DD MMMM") - - with self.assertRaises(ParserError): - self.parser.parse("68096653015/01/19", "YY/M/DD") - - def test_parse_with_extra_words_at_start_and_end_invalid(self): - input_format_pairs = [ - ("blah2016", "YYYY"), - ("blah2016blah", "YYYY"), - ("2016blah", "YYYY"), - ("2016-05blah", "YYYY-MM"), - ("2016-05-16blah", "YYYY-MM-DD"), - ("2016-05-16T04:05:06.789120blah", "YYYY-MM-DDThh:mm:ss.S"), - ("2016-05-16T04:05:06.789120ZblahZ", "YYYY-MM-DDThh:mm:ss.SZ"), - ("2016-05-16T04:05:06.789120Zblah", "YYYY-MM-DDThh:mm:ss.SZ"), - ("2016-05-16T04:05:06.789120blahZ", "YYYY-MM-DDThh:mm:ss.SZ"), - ] - - for pair in input_format_pairs: - with self.assertRaises(ParserError): - self.parser.parse(pair[0], pair[1]) - - def test_parse_with_extra_words_at_start_and_end_valid(self): - # Spaces surrounding the parsable date are ok because we - # allow the parsing of natural language input. Additionally, a single - # character of specific punctuation before or after the date is okay. - # See docs for full list of valid punctuation. - - self.assertEqual( - self.parser.parse("blah 2016 blah", "YYYY"), datetime(2016, 1, 1) - ) - - self.assertEqual(self.parser.parse("blah 2016", "YYYY"), datetime(2016, 1, 1)) - - self.assertEqual(self.parser.parse("2016 blah", "YYYY"), datetime(2016, 1, 1)) - - # test one additional space along with space divider - self.assertEqual( - self.parser.parse( - "blah 2016-05-16 04:05:06.789120", "YYYY-MM-DD hh:mm:ss.S" - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) - - self.assertEqual( - self.parser.parse( - "2016-05-16 04:05:06.789120 blah", "YYYY-MM-DD hh:mm:ss.S" - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) - - # test one additional space along with T divider - self.assertEqual( - self.parser.parse( - "blah 2016-05-16T04:05:06.789120", "YYYY-MM-DDThh:mm:ss.S" - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) - - self.assertEqual( - self.parser.parse( - "2016-05-16T04:05:06.789120 blah", "YYYY-MM-DDThh:mm:ss.S" - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) - - self.assertEqual( - 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), - ) - - self.assertEqual( - 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 - # for the list of valid punctuation, see documentation - def test_parse_with_punctuation_fences(self): - self.assertEqual( - self.parser.parse( - "Meet me at my house on Halloween (2019-31-10)", "YYYY-DD-MM" - ), - datetime(2019, 10, 31), - ) - - self.assertEqual( - self.parser.parse( - "Monday, 9. September 2019, 16:15-20:00", "dddd, D. MMMM YYYY" - ), - datetime(2019, 9, 9), - ) - - self.assertEqual( - self.parser.parse("A date is 11.11.2011.", "DD.MM.YYYY"), - datetime(2011, 11, 11), - ) - - with self.assertRaises(ParserMatchError): - self.parser.parse("11.11.2011.1 is not a valid date.", "DD.MM.YYYY") - - with self.assertRaises(ParserMatchError): - self.parser.parse( - "This date has too many punctuation marks following it (11.11.2011).", - "DD.MM.YYYY", - ) - - def test_parse_with_leading_and_trailing_whitespace(self): - self.assertEqual(self.parser.parse(" 2016", "YYYY"), datetime(2016, 1, 1)) - - self.assertEqual(self.parser.parse("2016 ", "YYYY"), datetime(2016, 1, 1)) - - self.assertEqual( - self.parser.parse(" 2016 ", "YYYY"), datetime(2016, 1, 1) - ) - - self.assertEqual( - self.parser.parse( - " 2016-05-16 04:05:06.789120 ", "YYYY-MM-DD hh:mm:ss.S" - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) - - self.assertEqual( - self.parser.parse( - " 2016-05-16T04:05:06.789120 ", "YYYY-MM-DDThh:mm:ss.S" - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) - - def test_parse_YYYY_DDDD(self): - self.assertEqual( - self.parser.parse("1998-136", "YYYY-DDDD"), datetime(1998, 5, 16) - ) - - self.assertEqual( - self.parser.parse("1998-006", "YYYY-DDDD"), datetime(1998, 1, 6) - ) - - with self.assertRaises(ParserError): - self.parser.parse("1998-456", "YYYY-DDDD") - - def test_parse_YYYY_DDD(self): - self.assertEqual(self.parser.parse("1998-6", "YYYY-DDD"), datetime(1998, 1, 6)) - - self.assertEqual( - self.parser.parse("1998-136", "YYYY-DDD"), datetime(1998, 5, 16) - ) - - with self.assertRaises(ParserError): - self.parser.parse("1998-756", "YYYY-DDD") - - # month cannot be passed with DDD and DDDD tokens - def test_parse_YYYY_MM_DDDD(self): - with self.assertRaises(ParserError): - self.parser.parse("2015-01-009", "YYYY-MM-DDDD") - - # year is required with the DDD and DDDD tokens - def test_parse_DDD_only(self): - with self.assertRaises(ParserError): - self.parser.parse("5", "DDD") - - def test_parse_DDDD_only(self): - with self.assertRaises(ParserError): - self.parser.parse("145", "DDDD") - - def test_parse_HH_24(self): - self.assertEqual( - self.parser.parse("2019-10-30T24:00:00", "YYYY-MM-DDTHH:mm:ss"), - datetime(2019, 10, 31, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse("2019-10-30T24:00", "YYYY-MM-DDTHH:mm"), - datetime(2019, 10, 31, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse("2019-10-30T24", "YYYY-MM-DDTHH"), - datetime(2019, 10, 31, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse("2019-10-30T24:00:00.0", "YYYY-MM-DDTHH:mm:ss.S"), - datetime(2019, 10, 31, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse("2019-10-31T24:00:00", "YYYY-MM-DDTHH:mm:ss"), - datetime(2019, 11, 1, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse("2019-12-31T24:00:00", "YYYY-MM-DDTHH:mm:ss"), - datetime(2020, 1, 1, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse("2019-12-31T23:59:59.9999999", "YYYY-MM-DDTHH:mm:ss.S"), - datetime(2020, 1, 1, 0, 0, 0, 0), - ) - - with self.assertRaises(ParserError): - self.parser.parse("2019-12-31T24:01:00", "YYYY-MM-DDTHH:mm:ss") - - with self.assertRaises(ParserError): - self.parser.parse("2019-12-31T24:00:01", "YYYY-MM-DDTHH:mm:ss") - - with self.assertRaises(ParserError): - self.parser.parse("2019-12-31T24:00:00.1", "YYYY-MM-DDTHH:mm:ss.S") - - with self.assertRaises(ParserError): - self.parser.parse("2019-12-31T24:00:00.999999", "YYYY-MM-DDTHH:mm:ss.S") - - -class DateTimeParserRegexTests(Chai): - def setUp(self): - super(DateTimeParserRegexTests, self).setUp() - - self.format_regex = parser.DateTimeParser._FORMAT_RE - - def test_format_year(self): - - self.assertEqual(self.format_regex.findall("YYYY-YY"), ["YYYY", "YY"]) - - def test_format_month(self): - - self.assertEqual( - self.format_regex.findall("MMMM-MMM-MM-M"), ["MMMM", "MMM", "MM", "M"] - ) - - def test_format_day(self): - - self.assertEqual( - self.format_regex.findall("DDDD-DDD-DD-D"), ["DDDD", "DDD", "DD", "D"] - ) - - def test_format_hour(self): - - self.assertEqual(self.format_regex.findall("HH-H-hh-h"), ["HH", "H", "hh", "h"]) - - def test_format_minute(self): - - self.assertEqual(self.format_regex.findall("mm-m"), ["mm", "m"]) - - def test_format_second(self): - - self.assertEqual(self.format_regex.findall("ss-s"), ["ss", "s"]) - - def test_format_subsecond(self): - - self.assertEqual( - self.format_regex.findall("SSSSSS-SSSSS-SSSS-SSS-SS-S"), - ["SSSSSS", "SSSSS", "SSSS", "SSS", "SS", "S"], - ) - - def test_format_tz(self): - - self.assertEqual(self.format_regex.findall("ZZZ-ZZ-Z"), ["ZZZ", "ZZ", "Z"]) - - def test_format_am_pm(self): - - self.assertEqual(self.format_regex.findall("A-a"), ["A", "a"]) - - def test_format_timestamp(self): - - self.assertEqual(self.format_regex.findall("X"), ["X"]) - - def test_format_timestamp_milli(self): - - self.assertEqual(self.format_regex.findall("x"), ["x"]) - - def test_escape(self): - - escape_regex = parser.DateTimeParser._ESCAPE_RE - - self.assertEqual( - escape_regex.findall("2018-03-09 8 [h] 40 [hello]"), ["[h]", "[hello]"] - ) - - def test_month_names(self): - p = parser.DateTimeParser("en_us") - - text = "_".join(calendar.month_name[1:]) - - result = p._input_re_map["MMMM"].findall(text) - - self.assertEqual(result, calendar.month_name[1:]) - - def test_month_abbreviations(self): - p = parser.DateTimeParser("en_us") - - text = "_".join(calendar.month_abbr[1:]) - - result = p._input_re_map["MMM"].findall(text) - - self.assertEqual(result, calendar.month_abbr[1:]) - - def test_digits(self): - - self.assertEqual( - parser.DateTimeParser._ONE_OR_TWO_DIGIT_RE.findall("4-56"), ["4", "56"] - ) - self.assertEqual( - parser.DateTimeParser._ONE_OR_TWO_OR_THREE_DIGIT_RE.findall("4-56-789"), - ["4", "56", "789"], - ) - self.assertEqual( - parser.DateTimeParser._ONE_OR_MORE_DIGIT_RE.findall("4-56-789-1234-12345"), - ["4", "56", "789", "1234", "12345"], - ) - self.assertEqual( - parser.DateTimeParser._TWO_DIGIT_RE.findall("12-3-45"), ["12", "45"] - ) - self.assertEqual( - parser.DateTimeParser._THREE_DIGIT_RE.findall("123-4-56"), ["123"] - ) - self.assertEqual( - parser.DateTimeParser._FOUR_DIGIT_RE.findall("1234-56"), ["1234"] - ) - - def test_tz(self): - tz_z_re = parser.DateTimeParser._TZ_Z_RE - self.assertEqual(tz_z_re.findall("-0700"), [("-", "07", "00")]) - self.assertEqual(tz_z_re.findall("+07"), [("+", "07", "")]) - self.assertTrue(tz_z_re.search("15/01/2019T04:05:06.789120Z") is not None) - self.assertTrue(tz_z_re.search("15/01/2019T04:05:06.789120") is None) - - tz_zz_re = parser.DateTimeParser._TZ_ZZ_RE - self.assertEqual(tz_zz_re.findall("-07:00"), [("-", "07", "00")]) - self.assertEqual(tz_zz_re.findall("+07"), [("+", "07", "")]) - self.assertTrue(tz_zz_re.search("15/01/2019T04:05:06.789120Z") is not None) - self.assertTrue(tz_zz_re.search("15/01/2019T04:05:06.789120") is None) - - tz_name_re = parser.DateTimeParser._TZ_NAME_RE - self.assertEqual(tz_name_re.findall("Europe/Warsaw"), ["Europe/Warsaw"]) - self.assertEqual(tz_name_re.findall("GMT"), ["GMT"]) - - def test_timestamp(self): - timestamp_re = parser.DateTimeParser._TIMESTAMP_RE - self.assertEqual( - timestamp_re.findall("1565707550.452729"), ["1565707550.452729"] - ) - self.assertEqual( - timestamp_re.findall("-1565707550.452729"), ["-1565707550.452729"] - ) - self.assertEqual(timestamp_re.findall("-1565707550"), ["-1565707550"]) - self.assertEqual(timestamp_re.findall("1565707550"), ["1565707550"]) - self.assertEqual(timestamp_re.findall("1565707550."), []) - self.assertEqual(timestamp_re.findall(".1565707550"), []) - - def test_timestamp_milli(self): - timestamp_expanded_re = parser.DateTimeParser._TIMESTAMP_EXPANDED_RE - self.assertEqual(timestamp_expanded_re.findall("-1565707550"), ["-1565707550"]) - self.assertEqual(timestamp_expanded_re.findall("1565707550"), ["1565707550"]) - self.assertEqual(timestamp_expanded_re.findall("1565707550.452729"), []) - self.assertEqual(timestamp_expanded_re.findall("1565707550."), []) - self.assertEqual(timestamp_expanded_re.findall(".1565707550"), []) - - def test_time(self): - time_re = parser.DateTimeParser._TIME_RE - time_seperators = [":", ""] - - for sep in time_seperators: - self.assertEqual(time_re.findall("12"), [("12", "", "", "", "")]) - self.assertEqual( - time_re.findall("12{sep}35".format(sep=sep)), [("12", "35", "", "", "")] - ) - self.assertEqual( - time_re.findall("12{sep}35{sep}46".format(sep=sep)), - [("12", "35", "46", "", "")], - ) - self.assertEqual( - time_re.findall("12{sep}35{sep}46.952313".format(sep=sep)), - [("12", "35", "46", ".", "952313")], - ) - self.assertEqual( - time_re.findall("12{sep}35{sep}46,952313".format(sep=sep)), - [("12", "35", "46", ",", "952313")], - ) - - self.assertEqual(time_re.findall("12:"), []) - self.assertEqual(time_re.findall("12:35:46."), []) - self.assertEqual(time_re.findall("12:35:46,"), []) - - -class DateTimeParserISOTests(Chai): - def setUp(self): - super(DateTimeParserISOTests, self).setUp() - - self.parser = parser.DateTimeParser("en_us") - - def test_YYYY(self): - - self.assertEqual(self.parser.parse_iso("2013"), datetime(2013, 1, 1)) - - def test_YYYY_DDDD(self): - self.assertEqual(self.parser.parse_iso("1998-136"), datetime(1998, 5, 16)) - - self.assertEqual(self.parser.parse_iso("1998-006"), datetime(1998, 1, 6)) - - with self.assertRaises(ParserError): - self.parser.parse_iso("1998-456") - - # 2016 is a leap year, so Feb 29 exists (leap day) - self.assertEqual(self.parser.parse_iso("2016-059"), datetime(2016, 2, 28)) - self.assertEqual(self.parser.parse_iso("2016-060"), datetime(2016, 2, 29)) - self.assertEqual(self.parser.parse_iso("2016-061"), datetime(2016, 3, 1)) - - # 2017 is not a leap year, so Feb 29 does not exist - self.assertEqual(self.parser.parse_iso("2017-059"), datetime(2017, 2, 28)) - self.assertEqual(self.parser.parse_iso("2017-060"), datetime(2017, 3, 1)) - self.assertEqual(self.parser.parse_iso("2017-061"), datetime(2017, 3, 2)) - - # Since 2016 is a leap year, the 366th day falls in the same year - self.assertEqual(self.parser.parse_iso("2016-366"), datetime(2016, 12, 31)) - - # Since 2017 is not a leap year, the 366th day falls in the next year - self.assertEqual(self.parser.parse_iso("2017-366"), datetime(2018, 1, 1)) - - def test_YYYY_DDDD_HH_mm_ssZ(self): - - self.assertEqual( - self.parser.parse_iso("2013-036 04:05:06+01:00"), - datetime(2013, 2, 5, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600)), - ) - - self.assertEqual( - self.parser.parse_iso("2013-036 04:05:06Z"), - datetime(2013, 2, 5, 4, 5, 6, tzinfo=tz.tzutc()), - ) - - def test_YYYY_MM_DDDD(self): - with self.assertRaises(ParserError): - self.parser.parse_iso("2014-05-125") - - def test_YYYY_MM(self): - - for separator in DateTimeParser.SEPARATORS: - self.assertEqual( - self.parser.parse_iso(separator.join(("2013", "02"))), - datetime(2013, 2, 1), - ) - - def test_YYYY_MM_DD(self): - - for separator in DateTimeParser.SEPARATORS: - self.assertEqual( - self.parser.parse_iso(separator.join(("2013", "02", "03"))), - datetime(2013, 2, 3), - ) - - def test_YYYY_MM_DDTHH_mmZ(self): - - self.assertEqual( - 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): - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05"), datetime(2013, 2, 3, 4, 5) - ) - - def test_YYYY_MM_DDTHH(self): - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04"), datetime(2013, 2, 3, 4) - ) - - def test_YYYY_MM_DDTHHZ(self): - - self.assertEqual( - 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): - - self.assertEqual( - 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): - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06"), datetime(2013, 2, 3, 4, 5, 6) - ) - - def test_YYYY_MM_DD_HH_mmZ(self): - - self.assertEqual( - 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): - - self.assertEqual( - self.parser.parse_iso("2013-02-03 04:05"), datetime(2013, 2, 3, 4, 5) - ) - - def test_YYYY_MM_DD_HH(self): - - self.assertEqual( - self.parser.parse_iso("2013-02-03 04"), datetime(2013, 2, 3, 4) - ) - - def test_invalid_time(self): - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03 044") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03 04:05:06.") - - def test_YYYY_MM_DD_HH_mm_ssZ(self): - - self.assertEqual( - 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): - - self.assertEqual( - 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): - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.7"), - datetime(2013, 2, 3, 4, 5, 6, 700000), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.78"), - datetime(2013, 2, 3, 4, 5, 6, 780000), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.789"), - datetime(2013, 2, 3, 4, 5, 6, 789000), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.7891"), - datetime(2013, 2, 3, 4, 5, 6, 789100), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.78912"), - datetime(2013, 2, 3, 4, 5, 6, 789120), - ) - - # ISO 8601:2004(E), ISO, 2004-12-01, 4.2.2.4 ... the decimal fraction - # shall be divided from the integer part by the decimal sign specified - # in ISO 31-0, i.e. the comma [,] or full stop [.]. Of these, the comma - # is the preferred sign. - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06,789123678"), - datetime(2013, 2, 3, 4, 5, 6, 789124), - ) - - # there is no limit on the number of decimal places - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.789123678"), - datetime(2013, 2, 3, 4, 5, 6, 789124), - ) - - def test_YYYY_MM_DDTHH_mm_ss_SZ(self): - - self.assertEqual( - 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)), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.78+01:00"), - datetime(2013, 2, 3, 4, 5, 6, 780000, tzinfo=tz.tzoffset(None, 3600)), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.789+01:00"), - datetime(2013, 2, 3, 4, 5, 6, 789000, tzinfo=tz.tzoffset(None, 3600)), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.7891+01:00"), - datetime(2013, 2, 3, 4, 5, 6, 789100, tzinfo=tz.tzoffset(None, 3600)), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.78912+01:00"), - datetime(2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzoffset(None, 3600)), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03 04:05:06.78912Z"), - datetime(2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzutc()), - ) - - def test_invalid_Z(self): - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912z") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912zz") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912Zz") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912ZZ") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912+Z") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912-Z") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912 Z") - - def test_parse_subsecond(self): - # TODO: make both test_parse_subsecond functions in Parse and ParseISO - # tests use the same expected objects (use pytest fixtures) - self.expected = datetime(2013, 1, 1, 12, 30, 45, 900000) - self.assertEqual(self.parser.parse_iso("2013-01-01 12:30:45.9"), self.expected) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 980000) - self.assertEqual(self.parser.parse_iso("2013-01-01 12:30:45.98"), self.expected) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987000) - self.assertEqual( - self.parser.parse_iso("2013-01-01 12:30:45.987"), self.expected - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987600) - self.assertEqual( - self.parser.parse_iso("2013-01-01 12:30:45.9876"), self.expected - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987650) - self.assertEqual( - self.parser.parse_iso("2013-01-01 12:30:45.98765"), self.expected - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - self.assertEqual( - self.parser.parse_iso("2013-01-01 12:30:45.987654"), self.expected - ) - - # use comma as subsecond separator - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - self.assertEqual( - self.parser.parse_iso("2013-01-01 12:30:45,987654"), self.expected - ) - - def test_gnu_date(self): - """ - regression tests for parsing output from GNU date(1) - """ - # date -Ins - self.assertEqual( - self.parser.parse_iso("2016-11-16T09:46:30,895636557-0800"), - datetime( - 2016, 11, 16, 9, 46, 30, 895636, tzinfo=tz.tzoffset(None, -3600 * 8) - ), - ) - - # date --rfc-3339=ns - self.assertEqual( - self.parser.parse_iso("2016-11-16 09:51:14.682141526-08:00"), - datetime( - 2016, 11, 16, 9, 51, 14, 682142, tzinfo=tz.tzoffset(None, -3600 * 8) - ), - ) - - def test_isoformat(self): - - dt = datetime.utcnow() - - self.assertEqual(self.parser.parse_iso(dt.isoformat()), dt) - - def test_parse_iso_with_leading_and_trailing_whitespace(self): - datetime_string = " 2016-11-15T06:37:19.123456" - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - datetime_string = " 2016-11-15T06:37:19.123456 " - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - datetime_string = "2016-11-15T06:37:19.123456 " - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - datetime_string = "2016-11-15T 06:37:19.123456" - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - # leading whitespace - datetime_string = " 2016-11-15 06:37:19.123456" - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - # trailing whitespace - datetime_string = "2016-11-15 06:37:19.123456 " - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - datetime_string = " 2016-11-15 06:37:19.123456 " - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - # two dividing spaces - datetime_string = "2016-11-15 06:37:19.123456" - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - def test_parse_iso_with_extra_words_at_start_and_end_invalid(self): - test_inputs = [ - "blah2016", - "blah2016blah", - "blah 2016 blah", - "blah 2016", - "2016 blah", - "blah 2016-05-16 04:05:06.789120", - "2016-05-16 04:05:06.789120 blah", - "blah 2016-05-16T04:05:06.789120", - "2016-05-16T04:05:06.789120 blah", - "2016blah", - "2016-05blah", - "2016-05-16blah", - "2016-05-16T04:05:06.789120blah", - "2016-05-16T04:05:06.789120ZblahZ", - "2016-05-16T04:05:06.789120Zblah", - "2016-05-16T04:05:06.789120blahZ", - "Meet me at 2016-05-16T04:05:06.789120 at the restaurant.", - "Meet me at 2016-05-16 04:05:06.789120 at the restaurant.", - ] - - for ti in test_inputs: - with self.assertRaises(ParserError): - self.parser.parse_iso(ti) - - def test_iso8601_basic_format(self): - self.assertEqual(self.parser.parse_iso("20180517"), datetime(2018, 5, 17)) - - self.assertEqual( - self.parser.parse_iso("20180517T10"), datetime(2018, 5, 17, 10) - ) - - self.assertEqual( - self.parser.parse_iso("20180517T105513.843456"), - datetime(2018, 5, 17, 10, 55, 13, 843456), - ) - - self.assertEqual( - self.parser.parse_iso("20180517T105513Z"), - datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzutc()), - ) - - self.assertEqual( - self.parser.parse_iso("20180517T105513.843456-0700"), - datetime(2018, 5, 17, 10, 55, 13, 843456, tzinfo=tz.tzoffset(None, -25200)), - ) - - self.assertEqual( - self.parser.parse_iso("20180517T105513-0700"), - datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), - ) - - self.assertEqual( - self.parser.parse_iso("20180517T105513-07"), - datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), - ) - - # ordinal in basic format: YYYYDDDD - self.assertEqual(self.parser.parse_iso("1998136"), datetime(1998, 5, 16)) - - # timezone requires +- seperator - with self.assertRaises(ParserError): - self.parser.parse_iso("20180517T1055130700") - - with self.assertRaises(ParserError): - self.parser.parse_iso("20180517T10551307") - - # too many digits in date - with self.assertRaises(ParserError): - self.parser.parse_iso("201860517T105513Z") - - # too many digits in time - with self.assertRaises(ParserError): - self.parser.parse_iso("20180517T1055213Z") - - def test_midnight_end_day(self): - self.assertEqual( - self.parser.parse_iso("2019-10-30T24:00:00"), - datetime(2019, 10, 31, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse_iso("2019-10-30T24:00"), - datetime(2019, 10, 31, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse_iso("2019-10-30T24:00:00.0"), - datetime(2019, 10, 31, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse_iso("2019-10-31T24:00:00"), - datetime(2019, 11, 1, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse_iso("2019-12-31T24:00:00"), - datetime(2020, 1, 1, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse_iso("2019-12-31T23:59:59.9999999"), - datetime(2020, 1, 1, 0, 0, 0, 0), - ) - - with self.assertRaises(ParserError): - self.parser.parse_iso("2019-12-31T24:01:00") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2019-12-31T24:00:01") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2019-12-31T24:00:00.1") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2019-12-31T24:00:00.999999") - - -class TzinfoParserTests(Chai): - def setUp(self): - super(TzinfoParserTests, self).setUp() - - self.parser = parser.TzinfoParser() - - def test_parse_local(self): - - self.assertEqual(self.parser.parse("local"), tz.tzlocal()) - - def test_parse_utc(self): - - self.assertEqual(self.parser.parse("utc"), tz.tzutc()) - self.assertEqual(self.parser.parse("UTC"), tz.tzutc()) - - def test_parse_iso(self): - - self.assertEqual(self.parser.parse("01:00"), tz.tzoffset(None, 3600)) - self.assertEqual( - self.parser.parse("11:35"), tz.tzoffset(None, 11 * 3600 + 2100) - ) - self.assertEqual(self.parser.parse("+01:00"), tz.tzoffset(None, 3600)) - self.assertEqual(self.parser.parse("-01:00"), tz.tzoffset(None, -3600)) - - self.assertEqual(self.parser.parse("0100"), tz.tzoffset(None, 3600)) - self.assertEqual(self.parser.parse("+0100"), tz.tzoffset(None, 3600)) - self.assertEqual(self.parser.parse("-0100"), tz.tzoffset(None, -3600)) - - self.assertEqual(self.parser.parse("01"), tz.tzoffset(None, 3600)) - self.assertEqual(self.parser.parse("+01"), tz.tzoffset(None, 3600)) - self.assertEqual(self.parser.parse("-01"), tz.tzoffset(None, -3600)) - - def test_parse_str(self): - - self.assertEqual(self.parser.parse("US/Pacific"), tz.gettz("US/Pacific")) - - def test_parse_fails(self): - - with self.assertRaises(parser.ParserError): - self.parser.parse("fail") - - -class DateTimeParserMonthNameTests(Chai): - def setUp(self): - super(DateTimeParserMonthNameTests, self).setUp() - - self.parser = parser.DateTimeParser("en_us") - - def test_shortmonth_capitalized(self): - - self.assertEqual( - self.parser.parse("2013-Jan-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) - ) - - def test_shortmonth_allupper(self): - - self.assertEqual( - self.parser.parse("2013-JAN-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) - ) - - def test_shortmonth_alllower(self): - - self.assertEqual( - self.parser.parse("2013-jan-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) - ) - - def test_month_capitalized(self): - - self.assertEqual( - self.parser.parse("2013-January-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) - ) - - def test_month_allupper(self): - - self.assertEqual( - self.parser.parse("2013-JANUARY-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) - ) - - def test_month_alllower(self): - - self.assertEqual( - self.parser.parse("2013-january-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) - ) - - def test_localized_month_name(self): - parser_ = parser.DateTimeParser("fr_fr") - - self.assertEqual( - parser_.parse("2013-Janvier-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) - ) - - def test_localized_month_abbreviation(self): - parser_ = parser.DateTimeParser("it_it") - - self.assertEqual( - parser_.parse("2013-Gen-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) - ) - - -class DateTimeParserMeridiansTests(Chai): - def setUp(self): - super(DateTimeParserMeridiansTests, self).setUp() - - self.parser = parser.DateTimeParser("en_us") - - def test_meridians_lowercase(self): - self.assertEqual( - self.parser.parse("2013-01-01 5am", "YYYY-MM-DD ha"), - datetime(2013, 1, 1, 5), - ) - - self.assertEqual( - self.parser.parse("2013-01-01 5pm", "YYYY-MM-DD ha"), - datetime(2013, 1, 1, 17), - ) - - def test_meridians_capitalized(self): - self.assertEqual( - self.parser.parse("2013-01-01 5AM", "YYYY-MM-DD hA"), - datetime(2013, 1, 1, 5), - ) - - self.assertEqual( - self.parser.parse("2013-01-01 5PM", "YYYY-MM-DD hA"), - datetime(2013, 1, 1, 17), - ) - - def test_localized_meridians_lowercase(self): - parser_ = parser.DateTimeParser("hu_hu") - self.assertEqual( - parser_.parse("2013-01-01 5 de", "YYYY-MM-DD h a"), datetime(2013, 1, 1, 5) - ) - - self.assertEqual( - parser_.parse("2013-01-01 5 du", "YYYY-MM-DD h a"), datetime(2013, 1, 1, 17) - ) - - def test_localized_meridians_capitalized(self): - parser_ = parser.DateTimeParser("hu_hu") - self.assertEqual( - parser_.parse("2013-01-01 5 DE", "YYYY-MM-DD h A"), datetime(2013, 1, 1, 5) - ) - - self.assertEqual( - parser_.parse("2013-01-01 5 DU", "YYYY-MM-DD h A"), datetime(2013, 1, 1, 17) - ) - - # regression test for issue #607 - def test_es_meridians(self): - parser_ = parser.DateTimeParser("es") - - self.assertEqual( - parser_.parse("Junio 30, 2019 - 08:00 pm", "MMMM DD, YYYY - hh:mm a"), - datetime(2019, 6, 30, 20, 0), - ) - - with self.assertRaises(ParserError): - parser_.parse( - "Junio 30, 2019 - 08:00 pasdfasdfm", "MMMM DD, YYYY - hh:mm a" - ) - - def test_fr_meridians(self): - parser_ = parser.DateTimeParser("fr") - - # the French locale always uses a 24 hour clock, so it does not support meridians - with self.assertRaises(ParserError): - parser_.parse("Janvier 30, 2019 - 08:00 pm", "MMMM DD, YYYY - hh:mm a") - - -class DateTimeParserMonthOrdinalDayTests(Chai): - def setUp(self): - super(DateTimeParserMonthOrdinalDayTests, self).setUp() - - self.parser = parser.DateTimeParser("en_us") - - def test_english(self): - parser_ = parser.DateTimeParser("en_us") - - self.assertEqual( - parser_.parse("January 1st, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) - ) - self.assertEqual( - parser_.parse("January 2nd, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 2) - ) - self.assertEqual( - parser_.parse("January 3rd, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 3) - ) - self.assertEqual( - parser_.parse("January 4th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 4) - ) - self.assertEqual( - parser_.parse("January 11th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 11) - ) - self.assertEqual( - parser_.parse("January 12th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 12) - ) - self.assertEqual( - parser_.parse("January 13th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 13) - ) - self.assertEqual( - parser_.parse("January 21st, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 21) - ) - self.assertEqual( - parser_.parse("January 31st, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 31) - ) - - with self.assertRaises(ParserError): - parser_.parse("January 1th, 2013", "MMMM Do, YYYY") - - with self.assertRaises(ParserError): - parser_.parse("January 11st, 2013", "MMMM Do, YYYY") - - def test_italian(self): - parser_ = parser.DateTimeParser("it_it") - - self.assertEqual( - parser_.parse("Gennaio 1º, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) - ) - - def test_spanish(self): - parser_ = parser.DateTimeParser("es_es") - - self.assertEqual( - parser_.parse("Enero 1º, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) - ) - - def test_french(self): - parser_ = parser.DateTimeParser("fr_fr") - - self.assertEqual( - parser_.parse("Janvier 1er, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) - ) - - self.assertEqual( - parser_.parse("Janvier 2e, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 2) - ) - - self.assertEqual( - parser_.parse("Janvier 11e, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 11) - ) - - -class DateTimeParserSearchDateTests(Chai): - def setUp(self): - super(DateTimeParserSearchDateTests, self).setUp() - self.parser = parser.DateTimeParser() - - def test_parse_search(self): - - self.assertEqual( - 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): - - self.assertEqual( - 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), - ) - - self.assertEqual( - self.parser.parse( - "Call 01-02-03 on 79-01-01 12:05:10", "YY-MM-DD HH:mm:ss" - ), - datetime(1979, 1, 1, 12, 5, 10), - ) - - def test_parse_search_with_names(self): - - self.assertEqual( - self.parser.parse("June was born in May 1980", "MMMM YYYY"), - datetime(1980, 5, 1), - ) - - def test_parse_search_locale_with_names(self): - p = parser.DateTimeParser("sv_se") - - self.assertEqual( - p.parse("Jan föddes den 31 Dec 1980", "DD MMM YYYY"), datetime(1980, 12, 31) - ) - - self.assertEqual( - p.parse("Jag föddes den 25 Augusti 1975", "DD MMMM YYYY"), - datetime(1975, 8, 25), - ) - - def test_parse_search_fails(self): - - with self.assertRaises(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" - self.assertEqual( - self.parser.parse("Thursday, December 10, 2015 at 5:09pm", format), - datetime(2015, 12, 10, 17, 9), - ) - - format = "[MMMM] M D, YYYY [at] h:mma" - self.assertEqual( - self.parser.parse("MMMM 12 10, 2015 at 5:09pm", format), - datetime(2015, 12, 10, 17, 9), - ) - - format = "[It happened on] MMMM Do [in the year] YYYY [a long time ago]" - self.assertEqual( - self.parser.parse( - "It happened on November 25th in the year 1990 a long time ago", format - ), - datetime(1990, 11, 25), - ) - - format = "[It happened on] MMMM Do [in the][ year] YYYY [a long time ago]" - self.assertEqual( - self.parser.parse( - "It happened on November 25th in the year 1990 a long time ago", format - ), - datetime(1990, 11, 25), - ) - - format = "[I'm][ entirely][ escaped,][ weee!]" - self.assertEqual( - self.parser.parse("I'm entirely escaped, weee!", format), datetime(1, 1, 1) - ) - - # Special RegEx characters - format = "MMM DD, YYYY |^${}().*+?<>-& h:mm A" - self.assertEqual( - self.parser.parse("Dec 31, 2017 |^${}().*+?<>-& 2:00 AM", format), - datetime(2017, 12, 31, 2, 0), - ) diff --git a/tests/parser_tests.py.bak b/tests/parser_tests.py.bak deleted file mode 100644 index c72da044b..000000000 --- a/tests/parser_tests.py.bak +++ /dev/null @@ -1,1629 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -import calendar -import os -import time -from datetime import datetime - -from chai import Chai -from dateutil import tz - -from arrow import parser -from arrow.constants import MAX_TIMESTAMP_US -from arrow.parser import DateTimeParser, ParserError, ParserMatchError - -from .utils import make_full_tz_list - - -class DateTimeParserTests(Chai): - def setUp(self): - super(DateTimeParserTests, self).setUp() - - self.parser = parser.DateTimeParser() - - def test_parse_multiformat(self): - - mock_datetime = self.mock() - - self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserMatchError) - self.expect(self.parser.parse).args("str", "fmt_b").returns(mock_datetime) - - result = self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) - - self.assertEqual(result, mock_datetime) - - def test_parse_multiformat_all_fail(self): - - self.expect(self.parser.parse).args("str", "fmt_a").raises(ParserMatchError) - self.expect(self.parser.parse).args("str", "fmt_b").raises(ParserMatchError) - - with self.assertRaises(ParserError): - self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) - - def test_parse_multiformat_unself_expected_fail(self): - class UnselfExpectedError(Exception): - pass - - self.expect(self.parser.parse).args("str", "fmt_a").raises(UnselfExpectedError) - - with self.assertRaises(UnselfExpectedError): - self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) - - def test_parse_token_nonsense(self): - parts = {} - self.parser._parse_token("NONSENSE", "1900", parts) - self.assertEqual(parts, {}) - - def test_parse_token_invalid_meridians(self): - parts = {} - self.parser._parse_token("A", "a..m", parts) - self.assertEqual(parts, {}) - self.parser._parse_token("a", "p..m", parts) - self.assertEqual(parts, {}) - - def test_parser_no_caching(self): - - self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( - 100 - ) - self.parser = parser.DateTimeParser(cache_size=0) - for _ in range(100): - self.parser._generate_pattern_re("fmt_a") - - def test_parser_1_line_caching(self): - - self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( - 1 - ) - self.parser = parser.DateTimeParser(cache_size=1) - for _ in range(100): - self.parser._generate_pattern_re("fmt_a") - - self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_b").times( - 1 - ) - for _ in range(100): - self.parser._generate_pattern_re("fmt_a") - self.parser._generate_pattern_re("fmt_b") - - self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( - 1 - ) - for _ in range(100): - self.parser._generate_pattern_re("fmt_a") - - def test_parser_multiple_line_caching(self): - - self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( - 1 - ) - self.parser = parser.DateTimeParser(cache_size=2) - for _ in range(100): - self.parser._generate_pattern_re("fmt_a") - - self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_b").times( - 1 - ) - for _ in range(100): - self.parser._generate_pattern_re("fmt_a") - self.parser._generate_pattern_re("fmt_b") - - self.expect(parser.DateTimeParser, "_generate_pattern_re").args("fmt_a").times( - 0 - ) - for _ in range(100): - self.parser._generate_pattern_re("fmt_a") - - def test_YY_and_YYYY_format_list(self): - - self.assertEqual( - self.parser.parse("15/01/19", ["DD/MM/YY", "DD/MM/YYYY"]), - datetime(2019, 1, 15), - ) - - # Regression test for issue #580 - self.assertEqual( - self.parser.parse("15/01/2019", ["DD/MM/YY", "DD/MM/YYYY"]), - datetime(2019, 1, 15), - ) - - self.assertEqual( - 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 - self.assertEqual( - self.parser.parse( - "15 Jul 2000", - ["MM/DD/YYYY", "YYYY-MM-DD", "X", "DD-MMMM-YYYY", "D MMM YYYY"], - ), - datetime(2000, 7, 15), - ) - - with self.assertRaises(ParserError): - self.parser.parse("15 Jul", "X") - - -class DateTimeParserParseTests(Chai): - def setUp(self): - super(DateTimeParserParseTests, self).setUp() - - self.parser = parser.DateTimeParser() - - def test_parse_list(self): - - self.expect(self.parser._parse_multiformat).args( - "str", ["fmt_a", "fmt_b"] - ).returns("result") - - result = self.parser.parse("str", ["fmt_a", "fmt_b"]) - - self.assertEqual(result, "result") - - def test_parse_unrecognized_token(self): - - mock_input_re_map = self.mock(self.parser, "_input_re_map") - - self.expect(mock_input_re_map.__getitem__).args("YYYY").raises(KeyError) - - with self.assertRaises(parser.ParserError): - self.parser.parse("2013-01-01", "YYYY-MM-DD") - - def test_parse_parse_no_match(self): - - with self.assertRaises(ParserError): - self.parser.parse("01-01", "YYYY-MM-DD") - - def test_parse_separators(self): - - with self.assertRaises(ParserError): - self.parser.parse("1403549231", "YYYY-MM-DD") - - def test_parse_numbers(self): - - self.expected = datetime(2012, 1, 1, 12, 5, 10) - self.assertEqual( - self.parser.parse("2012-01-01 12:05:10", "YYYY-MM-DD HH:mm:ss"), - self.expected, - ) - - def test_parse_year_two_digit(self): - - self.expected = datetime(1979, 1, 1, 12, 5, 10) - self.assertEqual( - 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() - int_timestamp = int(time.time()) - self.expected = datetime.fromtimestamp(int_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:d}".format(int_timestamp), "X"), self.expected - ) - - float_timestamp = time.time() - self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:f}".format(float_timestamp), "X"), self.expected - ) - - # test handling of ns timestamp (arrow will round to 6 digits regardless) - self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:f}123".format(float_timestamp), "X"), self.expected - ) - - # test ps timestamp (arrow will round to 6 digits regardless) - self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:f}123456".format(float_timestamp), "X"), self.expected - ) - - # NOTE: negative timestamps cannot be handled by datetime on Window - # Must use timedelta to handle them. ref: https://stackoverflow.com/questions/36179914 - if os.name != "nt": - # regression test for issue #662 - negative_int_timestamp = -int_timestamp - self.expected = datetime.fromtimestamp(negative_int_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:d}".format(negative_int_timestamp), "X"), - self.expected, - ) - - negative_float_timestamp = -float_timestamp - self.expected = datetime.fromtimestamp(negative_float_timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:f}".format(negative_float_timestamp), "X"), - self.expected, - ) - - # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will - # break cases like "15 Jul 2000" and a format list (see issue #447) - with self.assertRaises(ParserError): - natural_lang_string = "Meet me at {} at the restaurant.".format( - float_timestamp - ) - self.parser.parse(natural_lang_string, "X") - - with self.assertRaises(ParserError): - self.parser.parse("1565982019.", "X") - - with self.assertRaises(ParserError): - self.parser.parse(".1565982019", "X") - - def test_parse_expanded_timestamp(self): - # test expanded timestamps that include milliseconds - # and microseconds as multiples rather than decimals - # requested in issue #357 - - tz_utc = tz.tzutc() - timestamp = 1569982581.413132 - timestamp_milli = int(round(timestamp * 1000)) - timestamp_micro = int(round(timestamp * 1000000)) - - # "x" token should parse integer timestamps below MAX_TIMESTAMP normally - self.expected = datetime.fromtimestamp(int(timestamp), tz=tz_utc) - self.assertEqual( - self.parser.parse("{:d}".format(int(timestamp)), "x"), self.expected - ) - - self.expected = datetime.fromtimestamp(round(timestamp, 3), tz=tz_utc) - self.assertEqual( - self.parser.parse("{:d}".format(timestamp_milli), "x"), self.expected - ) - - self.expected = datetime.fromtimestamp(timestamp, tz=tz_utc) - self.assertEqual( - self.parser.parse("{:d}".format(timestamp_micro), "x"), self.expected - ) - - # anything above max µs timestamp should fail - with self.assertRaises(ValueError): - self.parser.parse("{:d}".format(int(MAX_TIMESTAMP_US) + 1), "x") - - # floats are not allowed with the "x" token - with self.assertRaises(ParserMatchError): - self.parser.parse("{:f}".format(timestamp), "x") - - def test_parse_names(self): - - self.expected = datetime(2012, 1, 1) - - self.assertEqual( - self.parser.parse("January 1, 2012", "MMMM D, YYYY"), self.expected - ) - self.assertEqual(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) - self.assertEqual(self.parser.parse("1 pm", "H a"), self.expected) - self.assertEqual(self.parser.parse("1 pm", "h a"), self.expected) - - self.expected = datetime(1, 1, 1, 1, 0, 0) - self.assertEqual(self.parser.parse("1 am", "H A"), self.expected) - self.assertEqual(self.parser.parse("1 am", "h A"), self.expected) - - self.expected = datetime(1, 1, 1, 0, 0, 0) - self.assertEqual(self.parser.parse("12 am", "H A"), self.expected) - self.assertEqual(self.parser.parse("12 am", "h A"), self.expected) - - self.expected = datetime(1, 1, 1, 12, 0, 0) - self.assertEqual(self.parser.parse("12 pm", "H A"), self.expected) - self.assertEqual(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") - self.assertEqual(parsed, self.expected) - - def test_parse_tz_zz(self): - - self.expected = datetime(2013, 1, 1, tzinfo=tz.tzoffset(None, -7 * 3600)) - self.assertEqual( - self.parser.parse("2013-01-01 -07:00", "YYYY-MM-DD ZZ"), self.expected - ) - - def test_parse_tz_name_zzz(self): - for tz_name in make_full_tz_list(): - self.expected = datetime(2013, 1, 1, tzinfo=tz.gettz(tz_name)) - self.assertEqual( - self.parser.parse("2013-01-01 %s" % tz_name, "YYYY-MM-DD ZZZ"), - self.expected, - ) - - # note that offsets are not timezones - with self.assertRaises(ParserError): - self.parser.parse("2013-01-01 12:30:45.9+1000", "YYYY-MM-DDZZZ") - - with self.assertRaises(ParserError): - self.parser.parse("2013-01-01 12:30:45.9+10:00", "YYYY-MM-DDZZZ") - - with self.assertRaises(ParserError): - self.parser.parse("2013-01-01 12:30:45.9-10", "YYYY-MM-DDZZZ") - - def test_parse_subsecond(self): - # TODO: make both test_parse_subsecond functions in Parse and ParseISO - # tests use the same expected objects (use pytest fixtures) - self.expected = datetime(2013, 1, 1, 12, 30, 45, 900000) - self.assertEqual( - self.parser.parse("2013-01-01 12:30:45.9", "YYYY-MM-DD HH:mm:ss.S"), - self.expected, - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 980000) - self.assertEqual( - self.parser.parse("2013-01-01 12:30:45.98", "YYYY-MM-DD HH:mm:ss.SS"), - self.expected, - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987000) - self.assertEqual( - self.parser.parse("2013-01-01 12:30:45.987", "YYYY-MM-DD HH:mm:ss.SSS"), - self.expected, - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987600) - self.assertEqual( - self.parser.parse("2013-01-01 12:30:45.9876", "YYYY-MM-DD HH:mm:ss.SSSS"), - self.expected, - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987650) - self.assertEqual( - self.parser.parse("2013-01-01 12:30:45.98765", "YYYY-MM-DD HH:mm:ss.SSSSS"), - self.expected, - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - self.assertEqual( - self.parser.parse( - "2013-01-01 12:30:45.987654", "YYYY-MM-DD HH:mm:ss.SSSSSS" - ), - self.expected, - ) - - def test_parse_subsecond_rounding(self): - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - datetime_format = "YYYY-MM-DD HH:mm:ss.S" - - # round up - string = "2013-01-01 12:30:45.9876539" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - # round down - string = "2013-01-01 12:30:45.98765432" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - # round half-up - string = "2013-01-01 12:30:45.987653521" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - # round half-down - string = "2013-01-01 12:30:45.9876545210" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - # overflow (zero out the subseconds and increment the seconds) - # regression tests for issue #636 - def test_parse_subsecond_rounding_overflow(self): - datetime_format = "YYYY-MM-DD HH:mm:ss.S" - - self.expected = datetime(2013, 1, 1, 12, 30, 46) - string = "2013-01-01 12:30:45.9999995" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - self.expected = datetime(2013, 1, 1, 12, 31, 0) - string = "2013-01-01 12:30:59.9999999" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - self.expected = datetime(2013, 1, 2, 0, 0, 0) - string = "2013-01-01 23:59:59.9999999" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - # 6 digits should remain unrounded - self.expected = datetime(2013, 1, 1, 12, 30, 45, 999999) - string = "2013-01-01 12:30:45.999999" - self.assertEqual(self.parser.parse(string, datetime_format), self.expected) - self.assertEqual(self.parser.parse_iso(string), self.expected) - - # Regression tests for issue #560 - def test_parse_long_year(self): - with self.assertRaises(ParserError): - self.parser.parse("09 January 123456789101112", "DD MMMM YYYY") - - with self.assertRaises(ParserError): - self.parser.parse("123456789101112 09 January", "YYYY DD MMMM") - - with self.assertRaises(ParserError): - self.parser.parse("68096653015/01/19", "YY/M/DD") - - def test_parse_with_extra_words_at_start_and_end_invalid(self): - input_format_pairs = [ - ("blah2016", "YYYY"), - ("blah2016blah", "YYYY"), - ("2016blah", "YYYY"), - ("2016-05blah", "YYYY-MM"), - ("2016-05-16blah", "YYYY-MM-DD"), - ("2016-05-16T04:05:06.789120blah", "YYYY-MM-DDThh:mm:ss.S"), - ("2016-05-16T04:05:06.789120ZblahZ", "YYYY-MM-DDThh:mm:ss.SZ"), - ("2016-05-16T04:05:06.789120Zblah", "YYYY-MM-DDThh:mm:ss.SZ"), - ("2016-05-16T04:05:06.789120blahZ", "YYYY-MM-DDThh:mm:ss.SZ"), - ] - - for pair in input_format_pairs: - with self.assertRaises(ParserError): - self.parser.parse(pair[0], pair[1]) - - def test_parse_with_extra_words_at_start_and_end_valid(self): - # Spaces surrounding the parsable date are ok because we - # allow the parsing of natural language input. Additionally, a single - # character of specific punctuation before or after the date is okay. - # See docs for full list of valid punctuation. - - self.assertEqual( - self.parser.parse("blah 2016 blah", "YYYY"), datetime(2016, 1, 1) - ) - - self.assertEqual(self.parser.parse("blah 2016", "YYYY"), datetime(2016, 1, 1)) - - self.assertEqual(self.parser.parse("2016 blah", "YYYY"), datetime(2016, 1, 1)) - - # test one additional space along with space divider - self.assertEqual( - self.parser.parse( - "blah 2016-05-16 04:05:06.789120", "YYYY-MM-DD hh:mm:ss.S" - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) - - self.assertEqual( - self.parser.parse( - "2016-05-16 04:05:06.789120 blah", "YYYY-MM-DD hh:mm:ss.S" - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) - - # test one additional space along with T divider - self.assertEqual( - self.parser.parse( - "blah 2016-05-16T04:05:06.789120", "YYYY-MM-DDThh:mm:ss.S" - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) - - self.assertEqual( - self.parser.parse( - "2016-05-16T04:05:06.789120 blah", "YYYY-MM-DDThh:mm:ss.S" - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) - - self.assertEqual( - 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), - ) - - self.assertEqual( - 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 - # for the list of valid punctuation, see documentation - def test_parse_with_punctuation_fences(self): - self.assertEqual( - self.parser.parse( - "Meet me at my house on Halloween (2019-31-10)", "YYYY-DD-MM" - ), - datetime(2019, 10, 31), - ) - - self.assertEqual( - self.parser.parse( - "Monday, 9. September 2019, 16:15-20:00", "dddd, D. MMMM YYYY" - ), - datetime(2019, 9, 9), - ) - - self.assertEqual( - self.parser.parse("A date is 11.11.2011.", "DD.MM.YYYY"), - datetime(2011, 11, 11), - ) - - with self.assertRaises(ParserMatchError): - self.parser.parse("11.11.2011.1 is not a valid date.", "DD.MM.YYYY") - - with self.assertRaises(ParserMatchError): - self.parser.parse( - "This date has too many punctuation marks following it (11.11.2011).", - "DD.MM.YYYY", - ) - - def test_parse_with_leading_and_trailing_whitespace(self): - self.assertEqual(self.parser.parse(" 2016", "YYYY"), datetime(2016, 1, 1)) - - self.assertEqual(self.parser.parse("2016 ", "YYYY"), datetime(2016, 1, 1)) - - self.assertEqual( - self.parser.parse(" 2016 ", "YYYY"), datetime(2016, 1, 1) - ) - - self.assertEqual( - self.parser.parse( - " 2016-05-16 04:05:06.789120 ", "YYYY-MM-DD hh:mm:ss.S" - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) - - self.assertEqual( - self.parser.parse( - " 2016-05-16T04:05:06.789120 ", "YYYY-MM-DDThh:mm:ss.S" - ), - datetime(2016, 5, 16, 4, 5, 6, 789120), - ) - - def test_parse_YYYY_DDDD(self): - self.assertEqual( - self.parser.parse("1998-136", "YYYY-DDDD"), datetime(1998, 5, 16) - ) - - self.assertEqual( - self.parser.parse("1998-006", "YYYY-DDDD"), datetime(1998, 1, 6) - ) - - with self.assertRaises(ParserError): - self.parser.parse("1998-456", "YYYY-DDDD") - - def test_parse_YYYY_DDD(self): - self.assertEqual(self.parser.parse("1998-6", "YYYY-DDD"), datetime(1998, 1, 6)) - - self.assertEqual( - self.parser.parse("1998-136", "YYYY-DDD"), datetime(1998, 5, 16) - ) - - with self.assertRaises(ParserError): - self.parser.parse("1998-756", "YYYY-DDD") - - # month cannot be passed with DDD and DDDD tokens - def test_parse_YYYY_MM_DDDD(self): - with self.assertRaises(ParserError): - self.parser.parse("2015-01-009", "YYYY-MM-DDDD") - - # year is required with the DDD and DDDD tokens - def test_parse_DDD_only(self): - with self.assertRaises(ParserError): - self.parser.parse("5", "DDD") - - def test_parse_DDDD_only(self): - with self.assertRaises(ParserError): - self.parser.parse("145", "DDDD") - - def test_parse_HH_24(self): - self.assertEqual( - self.parser.parse("2019-10-30T24:00:00", "YYYY-MM-DDTHH:mm:ss"), - datetime(2019, 10, 31, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse("2019-10-30T24:00", "YYYY-MM-DDTHH:mm"), - datetime(2019, 10, 31, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse("2019-10-30T24", "YYYY-MM-DDTHH"), - datetime(2019, 10, 31, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse("2019-10-30T24:00:00.0", "YYYY-MM-DDTHH:mm:ss.S"), - datetime(2019, 10, 31, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse("2019-10-31T24:00:00", "YYYY-MM-DDTHH:mm:ss"), - datetime(2019, 11, 1, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse("2019-12-31T24:00:00", "YYYY-MM-DDTHH:mm:ss"), - datetime(2020, 1, 1, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse("2019-12-31T23:59:59.9999999", "YYYY-MM-DDTHH:mm:ss.S"), - datetime(2020, 1, 1, 0, 0, 0, 0), - ) - - with self.assertRaises(ParserError): - self.parser.parse("2019-12-31T24:01:00", "YYYY-MM-DDTHH:mm:ss") - - with self.assertRaises(ParserError): - self.parser.parse("2019-12-31T24:00:01", "YYYY-MM-DDTHH:mm:ss") - - with self.assertRaises(ParserError): - self.parser.parse("2019-12-31T24:00:00.1", "YYYY-MM-DDTHH:mm:ss.S") - - with self.assertRaises(ParserError): - self.parser.parse("2019-12-31T24:00:00.999999", "YYYY-MM-DDTHH:mm:ss.S") - - -class DateTimeParserRegexTests(Chai): - def setUp(self): - super(DateTimeParserRegexTests, self).setUp() - - self.format_regex = parser.DateTimeParser._FORMAT_RE - - def test_format_year(self): - - self.assertEqual(self.format_regex.findall("YYYY-YY"), ["YYYY", "YY"]) - - def test_format_month(self): - - self.assertEqual( - self.format_regex.findall("MMMM-MMM-MM-M"), ["MMMM", "MMM", "MM", "M"] - ) - - def test_format_day(self): - - self.assertEqual( - self.format_regex.findall("DDDD-DDD-DD-D"), ["DDDD", "DDD", "DD", "D"] - ) - - def test_format_hour(self): - - self.assertEqual(self.format_regex.findall("HH-H-hh-h"), ["HH", "H", "hh", "h"]) - - def test_format_minute(self): - - self.assertEqual(self.format_regex.findall("mm-m"), ["mm", "m"]) - - def test_format_second(self): - - self.assertEqual(self.format_regex.findall("ss-s"), ["ss", "s"]) - - def test_format_subsecond(self): - - self.assertEqual( - self.format_regex.findall("SSSSSS-SSSSS-SSSS-SSS-SS-S"), - ["SSSSSS", "SSSSS", "SSSS", "SSS", "SS", "S"], - ) - - def test_format_tz(self): - - self.assertEqual(self.format_regex.findall("ZZZ-ZZ-Z"), ["ZZZ", "ZZ", "Z"]) - - def test_format_am_pm(self): - - self.assertEqual(self.format_regex.findall("A-a"), ["A", "a"]) - - def test_format_timestamp(self): - - self.assertEqual(self.format_regex.findall("X"), ["X"]) - - def test_format_timestamp_milli(self): - - self.assertEqual(self.format_regex.findall("x"), ["x"]) - - def test_escape(self): - - escape_regex = parser.DateTimeParser._ESCAPE_RE - - self.assertEqual( - escape_regex.findall("2018-03-09 8 [h] 40 [hello]"), ["[h]", "[hello]"] - ) - - def test_month_names(self): - p = parser.DateTimeParser("en_us") - - text = "_".join(calendar.month_name[1:]) - - result = p._input_re_map["MMMM"].findall(text) - - self.assertEqual(result, calendar.month_name[1:]) - - def test_month_abbreviations(self): - p = parser.DateTimeParser("en_us") - - text = "_".join(calendar.month_abbr[1:]) - - result = p._input_re_map["MMM"].findall(text) - - self.assertEqual(result, calendar.month_abbr[1:]) - - def test_digits(self): - - self.assertEqual( - parser.DateTimeParser._ONE_OR_TWO_DIGIT_RE.findall("4-56"), ["4", "56"] - ) - self.assertEqual( - parser.DateTimeParser._ONE_OR_TWO_OR_THREE_DIGIT_RE.findall("4-56-789"), - ["4", "56", "789"], - ) - self.assertEqual( - parser.DateTimeParser._ONE_OR_MORE_DIGIT_RE.findall("4-56-789-1234-12345"), - ["4", "56", "789", "1234", "12345"], - ) - self.assertEqual( - parser.DateTimeParser._TWO_DIGIT_RE.findall("12-3-45"), ["12", "45"] - ) - self.assertEqual( - parser.DateTimeParser._THREE_DIGIT_RE.findall("123-4-56"), ["123"] - ) - self.assertEqual( - parser.DateTimeParser._FOUR_DIGIT_RE.findall("1234-56"), ["1234"] - ) - - def test_tz(self): - tz_z_re = parser.DateTimeParser._TZ_Z_RE - self.assertEqual(tz_z_re.findall("-0700"), [("-", "07", "00")]) - self.assertEqual(tz_z_re.findall("+07"), [("+", "07", "")]) - self.assertTrue(tz_z_re.search("15/01/2019T04:05:06.789120Z") is not None) - self.assertTrue(tz_z_re.search("15/01/2019T04:05:06.789120") is None) - - tz_zz_re = parser.DateTimeParser._TZ_ZZ_RE - self.assertEqual(tz_zz_re.findall("-07:00"), [("-", "07", "00")]) - self.assertEqual(tz_zz_re.findall("+07"), [("+", "07", "")]) - self.assertTrue(tz_zz_re.search("15/01/2019T04:05:06.789120Z") is not None) - self.assertTrue(tz_zz_re.search("15/01/2019T04:05:06.789120") is None) - - tz_name_re = parser.DateTimeParser._TZ_NAME_RE - self.assertEqual(tz_name_re.findall("Europe/Warsaw"), ["Europe/Warsaw"]) - self.assertEqual(tz_name_re.findall("GMT"), ["GMT"]) - - def test_timestamp(self): - timestamp_re = parser.DateTimeParser._TIMESTAMP_RE - self.assertEqual( - timestamp_re.findall("1565707550.452729"), ["1565707550.452729"] - ) - self.assertEqual( - timestamp_re.findall("-1565707550.452729"), ["-1565707550.452729"] - ) - self.assertEqual(timestamp_re.findall("-1565707550"), ["-1565707550"]) - self.assertEqual(timestamp_re.findall("1565707550"), ["1565707550"]) - self.assertEqual(timestamp_re.findall("1565707550."), []) - self.assertEqual(timestamp_re.findall(".1565707550"), []) - - def test_timestamp_milli(self): - timestamp_expanded_re = parser.DateTimeParser._TIMESTAMP_EXPANDED_RE - self.assertEqual(timestamp_expanded_re.findall("-1565707550"), ["-1565707550"]) - self.assertEqual(timestamp_expanded_re.findall("1565707550"), ["1565707550"]) - self.assertEqual(timestamp_expanded_re.findall("1565707550.452729"), []) - self.assertEqual(timestamp_expanded_re.findall("1565707550."), []) - self.assertEqual(timestamp_expanded_re.findall(".1565707550"), []) - - def test_time(self): - time_re = parser.DateTimeParser._TIME_RE - time_seperators = [":", ""] - - for sep in time_seperators: - self.assertEqual(time_re.findall("12"), [("12", "", "", "", "")]) - self.assertEqual( - time_re.findall("12{sep}35".format(sep=sep)), [("12", "35", "", "", "")] - ) - self.assertEqual( - time_re.findall("12{sep}35{sep}46".format(sep=sep)), - [("12", "35", "46", "", "")], - ) - self.assertEqual( - time_re.findall("12{sep}35{sep}46.952313".format(sep=sep)), - [("12", "35", "46", ".", "952313")], - ) - self.assertEqual( - time_re.findall("12{sep}35{sep}46,952313".format(sep=sep)), - [("12", "35", "46", ",", "952313")], - ) - - self.assertEqual(time_re.findall("12:"), []) - self.assertEqual(time_re.findall("12:35:46."), []) - self.assertEqual(time_re.findall("12:35:46,"), []) - - -class DateTimeParserISOTests(Chai): - def setUp(self): - super(DateTimeParserISOTests, self).setUp() - - self.parser = parser.DateTimeParser("en_us") - - def test_YYYY(self): - - self.assertEqual(self.parser.parse_iso("2013"), datetime(2013, 1, 1)) - - def test_YYYY_DDDD(self): - self.assertEqual(self.parser.parse_iso("1998-136"), datetime(1998, 5, 16)) - - self.assertEqual(self.parser.parse_iso("1998-006"), datetime(1998, 1, 6)) - - with self.assertRaises(ParserError): - self.parser.parse_iso("1998-456") - - # 2016 is a leap year, so Feb 29 exists (leap day) - self.assertEqual(self.parser.parse_iso("2016-059"), datetime(2016, 2, 28)) - self.assertEqual(self.parser.parse_iso("2016-060"), datetime(2016, 2, 29)) - self.assertEqual(self.parser.parse_iso("2016-061"), datetime(2016, 3, 1)) - - # 2017 is not a leap year, so Feb 29 does not exist - self.assertEqual(self.parser.parse_iso("2017-059"), datetime(2017, 2, 28)) - self.assertEqual(self.parser.parse_iso("2017-060"), datetime(2017, 3, 1)) - self.assertEqual(self.parser.parse_iso("2017-061"), datetime(2017, 3, 2)) - - # Since 2016 is a leap year, the 366th day falls in the same year - self.assertEqual(self.parser.parse_iso("2016-366"), datetime(2016, 12, 31)) - - # Since 2017 is not a leap year, the 366th day falls in the next year - self.assertEqual(self.parser.parse_iso("2017-366"), datetime(2018, 1, 1)) - - def test_YYYY_DDDD_HH_mm_ssZ(self): - - self.assertEqual( - self.parser.parse_iso("2013-036 04:05:06+01:00"), - datetime(2013, 2, 5, 4, 5, 6, tzinfo=tz.tzoffset(None, 3600)), - ) - - self.assertEqual( - self.parser.parse_iso("2013-036 04:05:06Z"), - datetime(2013, 2, 5, 4, 5, 6, tzinfo=tz.tzutc()), - ) - - def test_YYYY_MM_DDDD(self): - with self.assertRaises(ParserError): - self.parser.parse_iso("2014-05-125") - - def test_YYYY_MM(self): - - for separator in DateTimeParser.SEPARATORS: - self.assertEqual( - self.parser.parse_iso(separator.join(("2013", "02"))), - datetime(2013, 2, 1), - ) - - def test_YYYY_MM_DD(self): - - for separator in DateTimeParser.SEPARATORS: - self.assertEqual( - self.parser.parse_iso(separator.join(("2013", "02", "03"))), - datetime(2013, 2, 3), - ) - - def test_YYYY_MM_DDTHH_mmZ(self): - - self.assertEqual( - 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): - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05"), datetime(2013, 2, 3, 4, 5) - ) - - def test_YYYY_MM_DDTHH(self): - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04"), datetime(2013, 2, 3, 4) - ) - - def test_YYYY_MM_DDTHHZ(self): - - self.assertEqual( - 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): - - self.assertEqual( - 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): - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06"), datetime(2013, 2, 3, 4, 5, 6) - ) - - def test_YYYY_MM_DD_HH_mmZ(self): - - self.assertEqual( - 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): - - self.assertEqual( - self.parser.parse_iso("2013-02-03 04:05"), datetime(2013, 2, 3, 4, 5) - ) - - def test_YYYY_MM_DD_HH(self): - - self.assertEqual( - self.parser.parse_iso("2013-02-03 04"), datetime(2013, 2, 3, 4) - ) - - def test_invalid_time(self): - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03 044") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03 04:05:06.") - - def test_YYYY_MM_DD_HH_mm_ssZ(self): - - self.assertEqual( - 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): - - self.assertEqual( - 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): - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.7"), - datetime(2013, 2, 3, 4, 5, 6, 700000), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.78"), - datetime(2013, 2, 3, 4, 5, 6, 780000), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.789"), - datetime(2013, 2, 3, 4, 5, 6, 789000), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.7891"), - datetime(2013, 2, 3, 4, 5, 6, 789100), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.78912"), - datetime(2013, 2, 3, 4, 5, 6, 789120), - ) - - # ISO 8601:2004(E), ISO, 2004-12-01, 4.2.2.4 ... the decimal fraction - # shall be divided from the integer part by the decimal sign specified - # in ISO 31-0, i.e. the comma [,] or full stop [.]. Of these, the comma - # is the preferred sign. - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06,789123678"), - datetime(2013, 2, 3, 4, 5, 6, 789124), - ) - - # there is no limit on the number of decimal places - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.789123678"), - datetime(2013, 2, 3, 4, 5, 6, 789124), - ) - - def test_YYYY_MM_DDTHH_mm_ss_SZ(self): - - self.assertEqual( - 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)), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.78+01:00"), - datetime(2013, 2, 3, 4, 5, 6, 780000, tzinfo=tz.tzoffset(None, 3600)), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.789+01:00"), - datetime(2013, 2, 3, 4, 5, 6, 789000, tzinfo=tz.tzoffset(None, 3600)), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.7891+01:00"), - datetime(2013, 2, 3, 4, 5, 6, 789100, tzinfo=tz.tzoffset(None, 3600)), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03T04:05:06.78912+01:00"), - datetime(2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzoffset(None, 3600)), - ) - - self.assertEqual( - self.parser.parse_iso("2013-02-03 04:05:06.78912Z"), - datetime(2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzutc()), - ) - - def test_invalid_Z(self): - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912z") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912zz") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912Zz") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912ZZ") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912+Z") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912-Z") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2013-02-03T04:05:06.78912 Z") - - def test_parse_subsecond(self): - # TODO: make both test_parse_subsecond functions in Parse and ParseISO - # tests use the same expected objects (use pytest fixtures) - self.expected = datetime(2013, 1, 1, 12, 30, 45, 900000) - self.assertEqual(self.parser.parse_iso("2013-01-01 12:30:45.9"), self.expected) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 980000) - self.assertEqual(self.parser.parse_iso("2013-01-01 12:30:45.98"), self.expected) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987000) - self.assertEqual( - self.parser.parse_iso("2013-01-01 12:30:45.987"), self.expected - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987600) - self.assertEqual( - self.parser.parse_iso("2013-01-01 12:30:45.9876"), self.expected - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987650) - self.assertEqual( - self.parser.parse_iso("2013-01-01 12:30:45.98765"), self.expected - ) - - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - self.assertEqual( - self.parser.parse_iso("2013-01-01 12:30:45.987654"), self.expected - ) - - # use comma as subsecond separator - self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) - self.assertEqual( - self.parser.parse_iso("2013-01-01 12:30:45,987654"), self.expected - ) - - def test_gnu_date(self): - """ - regression tests for parsing output from GNU date(1) - """ - # date -Ins - self.assertEqual( - self.parser.parse_iso("2016-11-16T09:46:30,895636557-0800"), - datetime( - 2016, 11, 16, 9, 46, 30, 895636, tzinfo=tz.tzoffset(None, -3600 * 8) - ), - ) - - # date --rfc-3339=ns - self.assertEqual( - self.parser.parse_iso("2016-11-16 09:51:14.682141526-08:00"), - datetime( - 2016, 11, 16, 9, 51, 14, 682142, tzinfo=tz.tzoffset(None, -3600 * 8) - ), - ) - - def test_isoformat(self): - - dt = datetime.utcnow() - - self.assertEqual(self.parser.parse_iso(dt.isoformat()), dt) - - def test_parse_iso_with_leading_and_trailing_whitespace(self): - datetime_string = " 2016-11-15T06:37:19.123456" - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - datetime_string = " 2016-11-15T06:37:19.123456 " - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - datetime_string = "2016-11-15T06:37:19.123456 " - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - datetime_string = "2016-11-15T 06:37:19.123456" - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - # leading whitespace - datetime_string = " 2016-11-15 06:37:19.123456" - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - # trailing whitespace - datetime_string = "2016-11-15 06:37:19.123456 " - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - datetime_string = " 2016-11-15 06:37:19.123456 " - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - # two dividing spaces - datetime_string = "2016-11-15 06:37:19.123456" - with self.assertRaises(ParserError): - self.parser.parse_iso(datetime_string) - - def test_parse_iso_with_extra_words_at_start_and_end_invalid(self): - test_inputs = [ - "blah2016", - "blah2016blah", - "blah 2016 blah", - "blah 2016", - "2016 blah", - "blah 2016-05-16 04:05:06.789120", - "2016-05-16 04:05:06.789120 blah", - "blah 2016-05-16T04:05:06.789120", - "2016-05-16T04:05:06.789120 blah", - "2016blah", - "2016-05blah", - "2016-05-16blah", - "2016-05-16T04:05:06.789120blah", - "2016-05-16T04:05:06.789120ZblahZ", - "2016-05-16T04:05:06.789120Zblah", - "2016-05-16T04:05:06.789120blahZ", - "Meet me at 2016-05-16T04:05:06.789120 at the restaurant.", - "Meet me at 2016-05-16 04:05:06.789120 at the restaurant.", - ] - - for ti in test_inputs: - with self.assertRaises(ParserError): - self.parser.parse_iso(ti) - - def test_iso8601_basic_format(self): - self.assertEqual(self.parser.parse_iso("20180517"), datetime(2018, 5, 17)) - - self.assertEqual( - self.parser.parse_iso("20180517T10"), datetime(2018, 5, 17, 10) - ) - - self.assertEqual( - self.parser.parse_iso("20180517T105513.843456"), - datetime(2018, 5, 17, 10, 55, 13, 843456), - ) - - self.assertEqual( - self.parser.parse_iso("20180517T105513Z"), - datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzutc()), - ) - - self.assertEqual( - self.parser.parse_iso("20180517T105513.843456-0700"), - datetime(2018, 5, 17, 10, 55, 13, 843456, tzinfo=tz.tzoffset(None, -25200)), - ) - - self.assertEqual( - self.parser.parse_iso("20180517T105513-0700"), - datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), - ) - - self.assertEqual( - self.parser.parse_iso("20180517T105513-07"), - datetime(2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200)), - ) - - # ordinal in basic format: YYYYDDDD - self.assertEqual(self.parser.parse_iso("1998136"), datetime(1998, 5, 16)) - - # timezone requires +- seperator - with self.assertRaises(ParserError): - self.parser.parse_iso("20180517T1055130700") - - with self.assertRaises(ParserError): - self.parser.parse_iso("20180517T10551307") - - # too many digits in date - with self.assertRaises(ParserError): - self.parser.parse_iso("201860517T105513Z") - - # too many digits in time - with self.assertRaises(ParserError): - self.parser.parse_iso("20180517T1055213Z") - - def test_midnight_end_day(self): - self.assertEqual( - self.parser.parse_iso("2019-10-30T24:00:00"), - datetime(2019, 10, 31, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse_iso("2019-10-30T24:00"), - datetime(2019, 10, 31, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse_iso("2019-10-30T24:00:00.0"), - datetime(2019, 10, 31, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse_iso("2019-10-31T24:00:00"), - datetime(2019, 11, 1, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse_iso("2019-12-31T24:00:00"), - datetime(2020, 1, 1, 0, 0, 0, 0), - ) - self.assertEqual( - self.parser.parse_iso("2019-12-31T23:59:59.9999999"), - datetime(2020, 1, 1, 0, 0, 0, 0), - ) - - with self.assertRaises(ParserError): - self.parser.parse_iso("2019-12-31T24:01:00") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2019-12-31T24:00:01") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2019-12-31T24:00:00.1") - - with self.assertRaises(ParserError): - self.parser.parse_iso("2019-12-31T24:00:00.999999") - - -class TzinfoParserTests(Chai): - def setUp(self): - super(TzinfoParserTests, self).setUp() - - self.parser = parser.TzinfoParser() - - def test_parse_local(self): - - self.assertEqual(self.parser.parse("local"), tz.tzlocal()) - - def test_parse_utc(self): - - self.assertEqual(self.parser.parse("utc"), tz.tzutc()) - self.assertEqual(self.parser.parse("UTC"), tz.tzutc()) - - def test_parse_iso(self): - - self.assertEqual(self.parser.parse("01:00"), tz.tzoffset(None, 3600)) - self.assertEqual( - self.parser.parse("11:35"), tz.tzoffset(None, 11 * 3600 + 2100) - ) - self.assertEqual(self.parser.parse("+01:00"), tz.tzoffset(None, 3600)) - self.assertEqual(self.parser.parse("-01:00"), tz.tzoffset(None, -3600)) - - self.assertEqual(self.parser.parse("0100"), tz.tzoffset(None, 3600)) - self.assertEqual(self.parser.parse("+0100"), tz.tzoffset(None, 3600)) - self.assertEqual(self.parser.parse("-0100"), tz.tzoffset(None, -3600)) - - self.assertEqual(self.parser.parse("01"), tz.tzoffset(None, 3600)) - self.assertEqual(self.parser.parse("+01"), tz.tzoffset(None, 3600)) - self.assertEqual(self.parser.parse("-01"), tz.tzoffset(None, -3600)) - - def test_parse_str(self): - - self.assertEqual(self.parser.parse("US/Pacific"), tz.gettz("US/Pacific")) - - def test_parse_fails(self): - - with self.assertRaises(parser.ParserError): - self.parser.parse("fail") - - -class DateTimeParserMonthNameTests(Chai): - def setUp(self): - super(DateTimeParserMonthNameTests, self).setUp() - - self.parser = parser.DateTimeParser("en_us") - - def test_shortmonth_capitalized(self): - - self.assertEqual( - self.parser.parse("2013-Jan-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) - ) - - def test_shortmonth_allupper(self): - - self.assertEqual( - self.parser.parse("2013-JAN-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) - ) - - def test_shortmonth_alllower(self): - - self.assertEqual( - self.parser.parse("2013-jan-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) - ) - - def test_month_capitalized(self): - - self.assertEqual( - self.parser.parse("2013-January-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) - ) - - def test_month_allupper(self): - - self.assertEqual( - self.parser.parse("2013-JANUARY-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) - ) - - def test_month_alllower(self): - - self.assertEqual( - self.parser.parse("2013-january-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) - ) - - def test_localized_month_name(self): - parser_ = parser.DateTimeParser("fr_fr") - - self.assertEqual( - parser_.parse("2013-Janvier-01", "YYYY-MMMM-DD"), datetime(2013, 1, 1) - ) - - def test_localized_month_abbreviation(self): - parser_ = parser.DateTimeParser("it_it") - - self.assertEqual( - parser_.parse("2013-Gen-01", "YYYY-MMM-DD"), datetime(2013, 1, 1) - ) - - -class DateTimeParserMeridiansTests(Chai): - def setUp(self): - super(DateTimeParserMeridiansTests, self).setUp() - - self.parser = parser.DateTimeParser("en_us") - - def test_meridians_lowercase(self): - self.assertEqual( - self.parser.parse("2013-01-01 5am", "YYYY-MM-DD ha"), - datetime(2013, 1, 1, 5), - ) - - self.assertEqual( - self.parser.parse("2013-01-01 5pm", "YYYY-MM-DD ha"), - datetime(2013, 1, 1, 17), - ) - - def test_meridians_capitalized(self): - self.assertEqual( - self.parser.parse("2013-01-01 5AM", "YYYY-MM-DD hA"), - datetime(2013, 1, 1, 5), - ) - - self.assertEqual( - self.parser.parse("2013-01-01 5PM", "YYYY-MM-DD hA"), - datetime(2013, 1, 1, 17), - ) - - def test_localized_meridians_lowercase(self): - parser_ = parser.DateTimeParser("hu_hu") - self.assertEqual( - parser_.parse("2013-01-01 5 de", "YYYY-MM-DD h a"), datetime(2013, 1, 1, 5) - ) - - self.assertEqual( - parser_.parse("2013-01-01 5 du", "YYYY-MM-DD h a"), datetime(2013, 1, 1, 17) - ) - - def test_localized_meridians_capitalized(self): - parser_ = parser.DateTimeParser("hu_hu") - self.assertEqual( - parser_.parse("2013-01-01 5 DE", "YYYY-MM-DD h A"), datetime(2013, 1, 1, 5) - ) - - self.assertEqual( - parser_.parse("2013-01-01 5 DU", "YYYY-MM-DD h A"), datetime(2013, 1, 1, 17) - ) - - # regression test for issue #607 - def test_es_meridians(self): - parser_ = parser.DateTimeParser("es") - - self.assertEqual( - parser_.parse("Junio 30, 2019 - 08:00 pm", "MMMM DD, YYYY - hh:mm a"), - datetime(2019, 6, 30, 20, 0), - ) - - with self.assertRaises(ParserError): - parser_.parse( - "Junio 30, 2019 - 08:00 pasdfasdfm", "MMMM DD, YYYY - hh:mm a" - ) - - def test_fr_meridians(self): - parser_ = parser.DateTimeParser("fr") - - # the French locale always uses a 24 hour clock, so it does not support meridians - with self.assertRaises(ParserError): - parser_.parse("Janvier 30, 2019 - 08:00 pm", "MMMM DD, YYYY - hh:mm a") - - -class DateTimeParserMonthOrdinalDayTests(Chai): - def setUp(self): - super(DateTimeParserMonthOrdinalDayTests, self).setUp() - - self.parser = parser.DateTimeParser("en_us") - - def test_english(self): - parser_ = parser.DateTimeParser("en_us") - - self.assertEqual( - parser_.parse("January 1st, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) - ) - self.assertEqual( - parser_.parse("January 2nd, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 2) - ) - self.assertEqual( - parser_.parse("January 3rd, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 3) - ) - self.assertEqual( - parser_.parse("January 4th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 4) - ) - self.assertEqual( - parser_.parse("January 11th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 11) - ) - self.assertEqual( - parser_.parse("January 12th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 12) - ) - self.assertEqual( - parser_.parse("January 13th, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 13) - ) - self.assertEqual( - parser_.parse("January 21st, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 21) - ) - self.assertEqual( - parser_.parse("January 31st, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 31) - ) - - with self.assertRaises(ParserError): - parser_.parse("January 1th, 2013", "MMMM Do, YYYY") - - with self.assertRaises(ParserError): - parser_.parse("January 11st, 2013", "MMMM Do, YYYY") - - def test_italian(self): - parser_ = parser.DateTimeParser("it_it") - - self.assertEqual( - parser_.parse("Gennaio 1º, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) - ) - - def test_spanish(self): - parser_ = parser.DateTimeParser("es_es") - - self.assertEqual( - parser_.parse("Enero 1º, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) - ) - - def test_french(self): - parser_ = parser.DateTimeParser("fr_fr") - - self.assertEqual( - parser_.parse("Janvier 1er, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 1) - ) - - self.assertEqual( - parser_.parse("Janvier 2e, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 2) - ) - - self.assertEqual( - parser_.parse("Janvier 11e, 2013", "MMMM Do, YYYY"), datetime(2013, 1, 11) - ) - - -class DateTimeParserSearchDateTests(Chai): - def setUp(self): - super(DateTimeParserSearchDateTests, self).setUp() - self.parser = parser.DateTimeParser() - - def test_parse_search(self): - - self.assertEqual( - 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): - - self.assertEqual( - 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), - ) - - self.assertEqual( - self.parser.parse( - "Call 01-02-03 on 79-01-01 12:05:10", "YY-MM-DD HH:mm:ss" - ), - datetime(1979, 1, 1, 12, 5, 10), - ) - - def test_parse_search_with_names(self): - - self.assertEqual( - self.parser.parse("June was born in May 1980", "MMMM YYYY"), - datetime(1980, 5, 1), - ) - - def test_parse_search_locale_with_names(self): - p = parser.DateTimeParser("sv_se") - - self.assertEqual( - p.parse("Jan föddes den 31 Dec 1980", "DD MMM YYYY"), datetime(1980, 12, 31) - ) - - self.assertEqual( - p.parse("Jag föddes den 25 Augusti 1975", "DD MMMM YYYY"), - datetime(1975, 8, 25), - ) - - def test_parse_search_fails(self): - - with self.assertRaises(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" - self.assertEqual( - self.parser.parse("Thursday, December 10, 2015 at 5:09pm", format), - datetime(2015, 12, 10, 17, 9), - ) - - format = "[MMMM] M D, YYYY [at] h:mma" - self.assertEqual( - self.parser.parse("MMMM 12 10, 2015 at 5:09pm", format), - datetime(2015, 12, 10, 17, 9), - ) - - format = "[It happened on] MMMM Do [in the year] YYYY [a long time ago]" - self.assertEqual( - self.parser.parse( - "It happened on November 25th in the year 1990 a long time ago", format - ), - datetime(1990, 11, 25), - ) - - format = "[It happened on] MMMM Do [in the][ year] YYYY [a long time ago]" - self.assertEqual( - self.parser.parse( - "It happened on November 25th in the year 1990 a long time ago", format - ), - datetime(1990, 11, 25), - ) - - format = "[I'm][ entirely][ escaped,][ weee!]" - self.assertEqual( - self.parser.parse("I'm entirely escaped, weee!", format), datetime(1, 1, 1) - ) - - # Special RegEx characters - format = "MMM DD, YYYY |^${}().*+?<>-& h:mm A" - self.assertEqual( - self.parser.parse("Dec 31, 2017 |^${}().*+?<>-& 2:00 AM", format), - datetime(2017, 12, 31, 2, 0), - ) diff --git a/tests/api_tests.py b/tests/test_api.py similarity index 100% rename from tests/api_tests.py rename to tests/test_api.py diff --git a/tests/arrow_tests.py b/tests/test_arrow.py similarity index 98% rename from tests/arrow_tests.py rename to tests/test_arrow.py index 561b81973..58c035e00 100644 --- a/tests/arrow_tests.py +++ b/tests/test_arrow.py @@ -13,12 +13,9 @@ from dateutil import tz from dateutil.relativedelta import FR, MO, SA, SU, TH, TU, WE -from arrow import arrow, util +from arrow import arrow - -def assertDtEqual(dt1, dt2, within=10): - assert dt1.tzinfo == dt2.tzinfo - assert abs(util.total_seconds(dt1 - dt2)) < within +from .utils import assert_datetime_equality class TestTestArrowInit: @@ -73,7 +70,7 @@ def test_init_pytz_timezone(self): 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Paris") ) assert result._datetime == self.expected - assertDtEqual(result._datetime, self.expected, 1) + assert_datetime_equality(result._datetime, self.expected, 1) class TestTestArrowFactory: @@ -81,29 +78,35 @@ def test_now(self): result = arrow.Arrow.now() - assertDtEqual(result._datetime, datetime.now().replace(tzinfo=tz.tzlocal())) + assert_datetime_equality( + result._datetime, datetime.now().replace(tzinfo=tz.tzlocal()) + ) def test_utcnow(self): result = arrow.Arrow.utcnow() - assertDtEqual(result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc())) + assert_datetime_equality( + result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc()) + ) def test_fromtimestamp(self): timestamp = time.time() result = arrow.Arrow.fromtimestamp(timestamp) - assertDtEqual(result._datetime, datetime.now().replace(tzinfo=tz.tzlocal())) + assert_datetime_equality( + result._datetime, datetime.now().replace(tzinfo=tz.tzlocal()) + ) result = arrow.Arrow.fromtimestamp(timestamp, tzinfo=tz.gettz("Europe/Paris")) - assertDtEqual( + assert_datetime_equality( result._datetime, datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), ) result = arrow.Arrow.fromtimestamp(timestamp, tzinfo="Europe/Paris") - assertDtEqual( + assert_datetime_equality( result._datetime, datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris")), ) @@ -116,7 +119,9 @@ def test_utcfromtimestamp(self): timestamp = time.time() result = arrow.Arrow.utcfromtimestamp(timestamp) - assertDtEqual(result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc())) + assert_datetime_equality( + result._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc()) + ) with pytest.raises(ValueError): arrow.Arrow.utcfromtimestamp("invalid timestamp") diff --git a/tests/factory_tests.py b/tests/test_factory.py similarity index 89% rename from tests/factory_tests.py rename to tests/test_factory.py index 840e9651c..a94a5ade9 100644 --- a/tests/factory_tests.py +++ b/tests/test_factory.py @@ -3,16 +3,13 @@ from datetime import date, datetime import dateparser +import pytest from dateutil import tz -from arrow import factory, util +from arrow import factory from arrow.parser import ParserError -import pytest - -def assertDtEqual(dt1, dt2, within=10): - assert dt1.tzinfo == dt2.tzinfo - assert abs(util.total_seconds(dt1 - dt2)) < within +from .utils import assert_datetime_equality class TestGet: @@ -22,7 +19,9 @@ def setup_class(cls): def test_no_args(self): - assertDtEqual(self.factory.get(), datetime.utcnow().replace(tzinfo=tz.tzutc())) + assert_datetime_equality( + self.factory.get(), datetime.utcnow().replace(tzinfo=tz.tzutc()) + ) def test_timestamp_one_arg_no_arg(self): @@ -33,13 +32,13 @@ def test_timestamp_one_arg_no_arg(self): def test_one_arg_none(self): - assertDtEqual( + assert_datetime_equality( self.factory.get(None), datetime.utcnow().replace(tzinfo=tz.tzutc()) ) def test_struct_time(self): - assertDtEqual( + assert_datetime_equality( self.factory.get(time.gmtime()), datetime.utcnow().replace(tzinfo=tz.tzutc()), ) @@ -80,7 +79,9 @@ def test_one_arg_timestamp_with_tzinfo(self): ) timezone = tz.gettz("US/Pacific") - assertDtEqual(self.factory.get(timestamp, tzinfo=timezone), timestamp_dt) + assert_datetime_equality( + self.factory.get(timestamp, tzinfo=timezone), timestamp_dt + ) def test_one_arg_arrow(self): @@ -110,7 +111,9 @@ def test_one_arg_tzinfo(self): .astimezone(tz.gettz("US/Pacific")) ) - assertDtEqual(self.factory.get(tz.gettz("US/Pacific")), self.expected) + assert_datetime_equality( + self.factory.get(tz.gettz("US/Pacific")), self.expected + ) # regression test for issue #658 def test_one_arg_dateparser_datetime(self): @@ -128,7 +131,9 @@ def test_kwarg_tzinfo(self): .astimezone(tz.gettz("US/Pacific")) ) - assertDtEqual(self.factory.get(tzinfo=tz.gettz("US/Pacific")), self.expected) + assert_datetime_equality( + self.factory.get(tzinfo=tz.gettz("US/Pacific")), self.expected + ) def test_kwarg_tzinfo_string(self): @@ -138,7 +143,7 @@ def test_kwarg_tzinfo_string(self): .astimezone(tz.gettz("US/Pacific")) ) - assertDtEqual(self.factory.get(tzinfo="US/Pacific"), self.expected) + assert_datetime_equality(self.factory.get(tzinfo="US/Pacific"), self.expected) with pytest.raises(ParserError): self.factory.get(tzinfo="US/PacificInvalidTzinfo") @@ -147,7 +152,9 @@ def test_one_arg_iso_str(self): dt = datetime.utcnow() - assertDtEqual(self.factory.get(dt.isoformat()), dt.replace(tzinfo=tz.tzutc())) + assert_datetime_equality( + self.factory.get(dt.isoformat()), dt.replace(tzinfo=tz.tzutc()) + ) def test_one_arg_iso_calendar(self): @@ -235,7 +242,7 @@ def test_two_args_str_tzinfo(self): result = self.factory.get("2013-01-01", tzinfo=tz.gettz("US/Pacific")) - assertDtEqual( + assert_datetime_equality( result._datetime, datetime(2013, 1, 1, tzinfo=tz.gettz("US/Pacific")) ) @@ -333,7 +340,7 @@ def setup_class(cls): def test_utcnow(self): - assertDtEqual( + assert_datetime_equality( self.factory.utcnow()._datetime, datetime.utcnow().replace(tzinfo=tz.tzutc()), ) @@ -346,12 +353,14 @@ def setup_class(cls): def test_no_tz(self): - assertDtEqual(self.factory.now(), datetime.now(tz.tzlocal())) + assert_datetime_equality(self.factory.now(), datetime.now(tz.tzlocal())) def test_tzinfo(self): - assertDtEqual(self.factory.now(tz.gettz("EST")), datetime.now(tz.gettz("EST"))) + assert_datetime_equality( + self.factory.now(tz.gettz("EST")), datetime.now(tz.gettz("EST")) + ) def test_tz_str(self): - assertDtEqual(self.factory.now("EST"), datetime.now(tz.gettz("EST"))) + assert_datetime_equality(self.factory.now("EST"), datetime.now(tz.gettz("EST"))) diff --git a/tests/formatter_tests.py b/tests/test_formatter.py similarity index 100% rename from tests/formatter_tests.py rename to tests/test_formatter.py diff --git a/tests/locales_tests.py b/tests/test_locales.py similarity index 98% rename from tests/locales_tests.py rename to tests/test_locales.py index 1b279ffa7..6002968f6 100644 --- a/tests/locales_tests.py +++ b/tests/test_locales.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from arrow import arrow, locales - import pytest +from arrow import arrow, locales + class TestLocaleValidation: """Validate locales to ensure that translations are valid and complete""" @@ -641,11 +641,17 @@ def test_relative_timeframes(self): assert self.locale._format_relative("o oră", "hour", 1) == "peste o oră" assert self.locale._format_relative("o oră", "hour", -1) == "o oră în urmă" assert self.locale._format_relative("un minut", "minute", 1) == "peste un minut" - assert self.locale._format_relative("un minut", "minute", -1) == "un minut în urmă" - assert self.locale._format_relative("câteva secunde", "seconds", -1) == \ - "câteva secunde în urmă" - assert self.locale._format_relative("câteva secunde", "seconds", 1) == \ - "peste câteva secunde" + assert ( + self.locale._format_relative("un minut", "minute", -1) == "un minut în urmă" + ) + assert ( + self.locale._format_relative("câteva secunde", "seconds", -1) + == "câteva secunde în urmă" + ) + assert ( + self.locale._format_relative("câteva secunde", "seconds", 1) + == "peste câteva secunde" + ) assert self.locale._format_relative("o zi", "day", -1) == "o zi în urmă" assert self.locale._format_relative("o zi", "day", 1) == "peste o zi" diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 000000000..039eb4c0a --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,1462 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import calendar +import os +import time +from datetime import datetime + +import pytest +from dateutil import tz + +import arrow +from arrow import parser +from arrow.constants import MAX_TIMESTAMP_US +from arrow.parser import DateTimeParser, ParserError, ParserMatchError + +from .utils import make_full_tz_list + + +class TestDateTimeParser: + @classmethod + def setup_class(cls): + cls.parser = parser.DateTimeParser() + + def test_parse_multiformat(self, mocker): + mocker.patch( + "arrow.parser.DateTimeParser.parse", + string="str", + fmt="fmt_a", + side_effect=parser.ParserError, + ) + + with pytest.raises(parser.ParserError): + self.parser._parse_multiformat("str", ["fmt_a"]) + + mock_datetime = mocker.Mock() + mocker.patch( + "arrow.parser.DateTimeParser.parse", + string="str", + fmt="fmt_b", + return_value=mock_datetime, + ) + + result = self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) + assert result == mock_datetime + + def test_parse_multiformat_all_fail(self, mocker): + mocker.patch( + "arrow.parser.DateTimeParser.parse", + string="str", + fmt="fmt_a", + side_effect=parser.ParserError, + ) + + mocker.patch( + "arrow.parser.DateTimeParser.parse", + string="str", + fmt="fmt_b", + side_effect=parser.ParserError, + ) + + with pytest.raises(parser.ParserError): + self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) + + def test_parse_multiformat_unself_expected_fail(self, mocker): + class UnselfExpectedError(Exception): + pass + + mocker.patch( + "arrow.parser.DateTimeParser.parse", + string="str", + fmt="fmt_a", + side_effect=UnselfExpectedError, + ) + + with pytest.raises(UnselfExpectedError): + self.parser._parse_multiformat("str", ["fmt_a", "fmt_b"]) + + def test_parse_token_nonsense(self): + parts = {} + self.parser._parse_token("NONSENSE", "1900", parts) + assert parts == {} + + def test_parse_token_invalid_meridians(self): + parts = {} + self.parser._parse_token("A", "a..m", parts) + assert parts == {} + self.parser._parse_token("a", "p..m", parts) + assert parts == {} + + def test_parser_no_caching(self, mocker): + + mocked_parser = mocker.patch( + "arrow.parser.DateTimeParser._generate_pattern_re", fmt="fmt_a" + ) + self.parser = parser.DateTimeParser(cache_size=0) + for _ in range(100): + self.parser._generate_pattern_re("fmt_a") + assert mocked_parser.call_count == 100 + + def test_parser_1_line_caching(self, mocker): + mocked_parser = mocker.patch("arrow.parser.DateTimeParser._generate_pattern_re") + self.parser = parser.DateTimeParser(cache_size=1) + + for _ in range(100): + self.parser._generate_pattern_re(fmt="fmt_a") + assert mocked_parser.call_count == 1 + assert mocked_parser.call_args_list[0] == mocker.call(fmt="fmt_a") + + for _ in range(100): + self.parser._generate_pattern_re(fmt="fmt_b") + assert mocked_parser.call_count == 2 + assert mocked_parser.call_args_list[1] == mocker.call(fmt="fmt_b") + + for _ in range(100): + self.parser._generate_pattern_re(fmt="fmt_a") + assert mocked_parser.call_count == 3 + assert mocked_parser.call_args_list[2] == mocker.call(fmt="fmt_a") + + def test_parser_multiple_line_caching(self, mocker): + mocked_parser = mocker.patch("arrow.parser.DateTimeParser._generate_pattern_re") + self.parser = parser.DateTimeParser(cache_size=2) + + for _ in range(100): + self.parser._generate_pattern_re(fmt="fmt_a") + assert mocked_parser.call_count == 1 + assert mocked_parser.call_args_list[0] == mocker.call(fmt="fmt_a") + + for _ in range(100): + self.parser._generate_pattern_re(fmt="fmt_b") + assert mocked_parser.call_count == 2 + assert mocked_parser.call_args_list[1] == mocker.call(fmt="fmt_b") + + # fmt_a and fmt_b are in the cache, so no new calls should be made + for _ in range(100): + self.parser._generate_pattern_re(fmt="fmt_a") + for _ in range(100): + self.parser._generate_pattern_re(fmt="fmt_b") + assert mocked_parser.call_count == 2 + assert mocked_parser.call_args_list[0] == mocker.call(fmt="fmt_a") + 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 + ) + + # Regression test for issue #580 + assert self.parser.parse("15/01/2019", ["DD/MM/YY", "DD/MM/YYYY"]) == datetime( + 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()) + + # 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) + + with pytest.raises(ParserError): + self.parser.parse("15 Jul", "X") + + +class TestDateTimeParserParse: + @classmethod + def setup_class(cls): + cls.parser = parser.DateTimeParser() + + def test_parse_list(self, mocker): + + mocker.patch( + "arrow.parser.DateTimeParser._parse_multiformat", + string="str", + formats=["fmt_a", "fmt_b"], + return_value="result", + ) + + result = self.parser.parse("str", ["fmt_a", "fmt_b"]) + 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"] + + # need to make another local parser to apply patch changes + _parser = parser.DateTimeParser() + with pytest.raises(parser.ParserError): + _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") + == self.expected + ) + + 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() + int_timestamp = int(time.time()) + self.expected = datetime.fromtimestamp(int_timestamp, tz=tz_utc) + assert self.parser.parse("{:d}".format(int_timestamp), "X") == self.expected + + float_timestamp = time.time() + self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) + assert self.parser.parse("{:f}".format(float_timestamp), "X") == self.expected + + # test handling of ns timestamp (arrow will round to 6 digits regardless) + self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) + assert ( + self.parser.parse("{:f}123".format(float_timestamp), "X") == self.expected + ) + + # test ps timestamp (arrow will round to 6 digits regardless) + self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) + assert ( + self.parser.parse("{:f}123456".format(float_timestamp), "X") + == self.expected + ) + + # NOTE: negative timestamps cannot be handled by datetime on Window + # Must use timedelta to handle them. ref: https://stackoverflow.com/questions/36179914 + if os.name != "nt": + # regression test for issue #662 + negative_int_timestamp = -int_timestamp + self.expected = datetime.fromtimestamp(negative_int_timestamp, tz=tz_utc) + assert ( + self.parser.parse("{:d}".format(negative_int_timestamp), "X") + == self.expected + ) + + negative_float_timestamp = -float_timestamp + self.expected = datetime.fromtimestamp(negative_float_timestamp, tz=tz_utc) + assert ( + self.parser.parse("{:f}".format(negative_float_timestamp), "X") + == self.expected + ) + + # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will + # break cases like "15 Jul 2000" and a format list (see issue #447) + with pytest.raises(ParserError): + natural_lang_string = "Meet me at {} at the restaurant.".format( + float_timestamp + ) + self.parser.parse(natural_lang_string, "X") + + with pytest.raises(ParserError): + self.parser.parse("1565982019.", "X") + + with pytest.raises(ParserError): + self.parser.parse(".1565982019", "X") + + def test_parse_expanded_timestamp(self): + # test expanded timestamps that include milliseconds + # and microseconds as multiples rather than decimals + # requested in issue #357 + + tz_utc = tz.tzutc() + timestamp = 1569982581.413132 + timestamp_milli = int(round(timestamp * 1000)) + timestamp_micro = int(round(timestamp * 1000000)) + + # "x" token should parse integer timestamps below MAX_TIMESTAMP normally + self.expected = datetime.fromtimestamp(int(timestamp), tz=tz_utc) + assert self.parser.parse("{:d}".format(int(timestamp)), "x") == self.expected + + self.expected = datetime.fromtimestamp(round(timestamp, 3), tz=tz_utc) + assert self.parser.parse("{:d}".format(timestamp_milli), "x") == self.expected + + self.expected = datetime.fromtimestamp(timestamp, tz=tz_utc) + assert self.parser.parse("{:d}".format(timestamp_micro), "x") == self.expected + + # anything above max µs timestamp should fail + with pytest.raises(ValueError): + self.parser.parse("{:d}".format(int(MAX_TIMESTAMP_US) + 1), "x") + + # floats are not allowed with the "x" token + with pytest.raises(ParserMatchError): + self.parser.parse("{:f}".format(timestamp), "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 + + self.expected = datetime(1, 1, 1, 1, 0, 0) + assert self.parser.parse("1 am", "H A") == self.expected + assert self.parser.parse("1 am", "h A") == self.expected + + self.expected = datetime(1, 1, 1, 0, 0, 0) + assert self.parser.parse("12 am", "H A") == self.expected + assert self.parser.parse("12 am", "h A") == self.expected + + self.expected = datetime(1, 1, 1, 12, 0, 0) + assert self.parser.parse("12 pm", "H A") == self.expected + 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 + + def test_parse_tz_name_zzz(self): + for tz_name in make_full_tz_list(): + self.expected = datetime(2013, 1, 1, tzinfo=tz.gettz(tz_name)) + assert ( + self.parser.parse("2013-01-01 %s" % tz_name, "YYYY-MM-DD ZZZ") + == self.expected + ) + + # note that offsets are not timezones + with pytest.raises(ParserError): + self.parser.parse("2013-01-01 12:30:45.9+1000", "YYYY-MM-DDZZZ") + + with pytest.raises(ParserError): + self.parser.parse("2013-01-01 12:30:45.9+10:00", "YYYY-MM-DDZZZ") + + with pytest.raises(ParserError): + self.parser.parse("2013-01-01 12:30:45.9-10", "YYYY-MM-DDZZZ") + + def test_parse_subsecond(self): + # TODO: make both test_parse_subsecond functions in Parse and ParseISO + # tests use the same expected objects (use pytest fixtures) + self.expected = datetime(2013, 1, 1, 12, 30, 45, 900000) + assert ( + self.parser.parse("2013-01-01 12:30:45.9", "YYYY-MM-DD HH:mm:ss.S") + == self.expected + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 980000) + assert ( + self.parser.parse("2013-01-01 12:30:45.98", "YYYY-MM-DD HH:mm:ss.SS") + == self.expected + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987000) + assert ( + self.parser.parse("2013-01-01 12:30:45.987", "YYYY-MM-DD HH:mm:ss.SSS") + == self.expected + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987600) + assert ( + self.parser.parse("2013-01-01 12:30:45.9876", "YYYY-MM-DD HH:mm:ss.SSSS") + == self.expected + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987650) + assert ( + self.parser.parse("2013-01-01 12:30:45.98765", "YYYY-MM-DD HH:mm:ss.SSSSS") + == self.expected + ) + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) + assert ( + self.parser.parse( + "2013-01-01 12:30:45.987654", "YYYY-MM-DD HH:mm:ss.SSSSSS" + ) + == self.expected + ) + + def test_parse_subsecond_rounding(self): + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) + datetime_format = "YYYY-MM-DD HH:mm:ss.S" + + # round up + string = "2013-01-01 12:30:45.9876539" + assert self.parser.parse(string, datetime_format) == self.expected + assert self.parser.parse_iso(string) == self.expected + + # round down + string = "2013-01-01 12:30:45.98765432" + assert self.parser.parse(string, datetime_format) == self.expected + assert self.parser.parse_iso(string) == self.expected + + # round half-up + string = "2013-01-01 12:30:45.987653521" + assert self.parser.parse(string, datetime_format) == self.expected + assert self.parser.parse_iso(string) == self.expected + + # round half-down + string = "2013-01-01 12:30:45.9876545210" + assert self.parser.parse(string, datetime_format) == self.expected + assert self.parser.parse_iso(string) == self.expected + + # overflow (zero out the subseconds and increment the seconds) + # regression tests for issue #636 + def test_parse_subsecond_rounding_overflow(self): + datetime_format = "YYYY-MM-DD HH:mm:ss.S" + + self.expected = datetime(2013, 1, 1, 12, 30, 46) + string = "2013-01-01 12:30:45.9999995" + assert self.parser.parse(string, datetime_format) == self.expected + assert self.parser.parse_iso(string) == self.expected + + self.expected = datetime(2013, 1, 1, 12, 31, 0) + string = "2013-01-01 12:30:59.9999999" + assert self.parser.parse(string, datetime_format) == self.expected + assert self.parser.parse_iso(string) == self.expected + + self.expected = datetime(2013, 1, 2, 0, 0, 0) + string = "2013-01-01 23:59:59.9999999" + assert self.parser.parse(string, datetime_format) == self.expected + assert self.parser.parse_iso(string) == self.expected + + # 6 digits should remain unrounded + self.expected = datetime(2013, 1, 1, 12, 30, 45, 999999) + string = "2013-01-01 12:30:45.999999" + assert self.parser.parse(string, datetime_format) == self.expected + assert self.parser.parse_iso(string) == self.expected + + # Regression tests for issue #560 + def test_parse_long_year(self): + with pytest.raises(ParserError): + self.parser.parse("09 January 123456789101112", "DD MMMM YYYY") + + with pytest.raises(ParserError): + self.parser.parse("123456789101112 09 January", "YYYY DD MMMM") + + with pytest.raises(ParserError): + self.parser.parse("68096653015/01/19", "YY/M/DD") + + def test_parse_with_extra_words_at_start_and_end_invalid(self): + input_format_pairs = [ + ("blah2016", "YYYY"), + ("blah2016blah", "YYYY"), + ("2016blah", "YYYY"), + ("2016-05blah", "YYYY-MM"), + ("2016-05-16blah", "YYYY-MM-DD"), + ("2016-05-16T04:05:06.789120blah", "YYYY-MM-DDThh:mm:ss.S"), + ("2016-05-16T04:05:06.789120ZblahZ", "YYYY-MM-DDThh:mm:ss.SZ"), + ("2016-05-16T04:05:06.789120Zblah", "YYYY-MM-DDThh:mm:ss.SZ"), + ("2016-05-16T04:05:06.789120blahZ", "YYYY-MM-DDThh:mm:ss.SZ"), + ] + + for pair in input_format_pairs: + with pytest.raises(ParserError): + self.parser.parse(pair[0], pair[1]) + + def test_parse_with_extra_words_at_start_and_end_valid(self): + # Spaces surrounding the parsable date are ok because we + # allow the parsing of natural language input. Additionally, a single + # character of specific punctuation before or after the date is okay. + # See docs for full list of valid punctuation. + + assert self.parser.parse("blah 2016 blah", "YYYY") == datetime(2016, 1, 1) + + assert self.parser.parse("blah 2016", "YYYY") == datetime(2016, 1, 1) + + assert self.parser.parse("2016 blah", "YYYY") == datetime(2016, 1, 1) + + # test one additional space along with space divider + assert self.parser.parse( + "blah 2016-05-16 04:05:06.789120", "YYYY-MM-DD hh:mm:ss.S" + ) == datetime(2016, 5, 16, 4, 5, 6, 789120) + + assert self.parser.parse( + "2016-05-16 04:05:06.789120 blah", "YYYY-MM-DD hh:mm:ss.S" + ) == datetime(2016, 5, 16, 4, 5, 6, 789120) + + # test one additional space along with T divider + assert self.parser.parse( + "blah 2016-05-16T04:05:06.789120", "YYYY-MM-DDThh:mm:ss.S" + ) == datetime(2016, 5, 16, 4, 5, 6, 789120) + + assert self.parser.parse( + "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-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 + # for the list of valid punctuation, see documentation + def test_parse_with_punctuation_fences(self): + assert self.parser.parse( + "Meet me at my house on Halloween (2019-31-10)", "YYYY-DD-MM" + ) == datetime(2019, 10, 31) + + assert self.parser.parse( + "Monday, 9. September 2019, 16:15-20:00", "dddd, D. MMMM YYYY" + ) == datetime(2019, 9, 9) + + assert self.parser.parse("A date is 11.11.2011.", "DD.MM.YYYY") == datetime( + 2011, 11, 11 + ) + + with pytest.raises(ParserMatchError): + self.parser.parse("11.11.2011.1 is not a valid date.", "DD.MM.YYYY") + + with pytest.raises(ParserMatchError): + self.parser.parse( + "This date has too many punctuation marks following it (11.11.2011).", + "DD.MM.YYYY", + ) + + def test_parse_with_leading_and_trailing_whitespace(self): + assert self.parser.parse(" 2016", "YYYY") == datetime(2016, 1, 1) + + assert self.parser.parse("2016 ", "YYYY") == datetime(2016, 1, 1) + + assert self.parser.parse(" 2016 ", "YYYY") == datetime(2016, 1, 1) + + assert self.parser.parse( + " 2016-05-16 04:05:06.789120 ", "YYYY-MM-DD hh:mm:ss.S" + ) == datetime(2016, 5, 16, 4, 5, 6, 789120) + + assert self.parser.parse( + " 2016-05-16T04:05:06.789120 ", "YYYY-MM-DDThh:mm:ss.S" + ) == datetime(2016, 5, 16, 4, 5, 6, 789120) + + def test_parse_YYYY_DDDD(self): + assert self.parser.parse("1998-136", "YYYY-DDDD") == datetime(1998, 5, 16) + + assert self.parser.parse("1998-006", "YYYY-DDDD") == datetime(1998, 1, 6) + + with pytest.raises(ParserError): + self.parser.parse("1998-456", "YYYY-DDDD") + + def test_parse_YYYY_DDD(self): + assert self.parser.parse("1998-6", "YYYY-DDD") == datetime(1998, 1, 6) + + assert self.parser.parse("1998-136", "YYYY-DDD") == datetime(1998, 5, 16) + + with pytest.raises(ParserError): + self.parser.parse("1998-756", "YYYY-DDD") + + # month cannot be passed with DDD and DDDD tokens + def test_parse_YYYY_MM_DDDD(self): + with pytest.raises(ParserError): + self.parser.parse("2015-01-009", "YYYY-MM-DDDD") + + # year is required with the DDD and DDDD tokens + def test_parse_DDD_only(self): + with pytest.raises(ParserError): + self.parser.parse("5", "DDD") + + def test_parse_DDDD_only(self): + with pytest.raises(ParserError): + self.parser.parse("145", "DDDD") + + def test_parse_HH_24(self): + assert self.parser.parse( + "2019-10-30T24:00:00", "YYYY-MM-DDTHH:mm:ss" + ) == datetime(2019, 10, 31, 0, 0, 0, 0) + assert self.parser.parse("2019-10-30T24:00", "YYYY-MM-DDTHH:mm") == datetime( + 2019, 10, 31, 0, 0, 0, 0 + ) + assert self.parser.parse("2019-10-30T24", "YYYY-MM-DDTHH") == datetime( + 2019, 10, 31, 0, 0, 0, 0 + ) + assert self.parser.parse( + "2019-10-30T24:00:00.0", "YYYY-MM-DDTHH:mm:ss.S" + ) == datetime(2019, 10, 31, 0, 0, 0, 0) + assert self.parser.parse( + "2019-10-31T24:00:00", "YYYY-MM-DDTHH:mm:ss" + ) == datetime(2019, 11, 1, 0, 0, 0, 0) + assert self.parser.parse( + "2019-12-31T24:00:00", "YYYY-MM-DDTHH:mm:ss" + ) == datetime(2020, 1, 1, 0, 0, 0, 0) + assert self.parser.parse( + "2019-12-31T23:59:59.9999999", "YYYY-MM-DDTHH:mm:ss.S" + ) == datetime(2020, 1, 1, 0, 0, 0, 0) + + with pytest.raises(ParserError): + self.parser.parse("2019-12-31T24:01:00", "YYYY-MM-DDTHH:mm:ss") + + with pytest.raises(ParserError): + self.parser.parse("2019-12-31T24:00:01", "YYYY-MM-DDTHH:mm:ss") + + with pytest.raises(ParserError): + self.parser.parse("2019-12-31T24:00:00.1", "YYYY-MM-DDTHH:mm:ss.S") + + with pytest.raises(ParserError): + self.parser.parse("2019-12-31T24:00:00.999999", "YYYY-MM-DDTHH:mm:ss.S") + + +class TestDateTimeParserRegex: + @classmethod + def setup_class(cls): + cls.format_regex = parser.DateTimeParser._FORMAT_RE + + 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", + "SSSS", + "SSS", + "SS", + "S", + ] + + 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]"] + + def test_month_names(self): + p = parser.DateTimeParser("en_us") + + text = "_".join(calendar.month_name[1:]) + + result = p._input_re_map["MMMM"].findall(text) + + assert result == calendar.month_name[1:] + + def test_month_abbreviations(self): + p = parser.DateTimeParser("en_us") + + text = "_".join(calendar.month_abbr[1:]) + + result = p._input_re_map["MMM"].findall(text) + + 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" + ) == ["4", "56", "789"] + assert parser.DateTimeParser._ONE_OR_MORE_DIGIT_RE.findall( + "4-56-789-1234-12345" + ) == ["4", "56", "789", "1234", "12345"] + assert parser.DateTimeParser._TWO_DIGIT_RE.findall("12-3-45") == ["12", "45"] + assert parser.DateTimeParser._THREE_DIGIT_RE.findall("123-4-56") == ["123"] + assert parser.DateTimeParser._FOUR_DIGIT_RE.findall("1234-56") == ["1234"] + + def test_tz(self): + tz_z_re = parser.DateTimeParser._TZ_Z_RE + assert tz_z_re.findall("-0700") == [("-", "07", "00")] + assert tz_z_re.findall("+07") == [("+", "07", "")] + assert tz_z_re.search("15/01/2019T04:05:06.789120Z") is not None + assert tz_z_re.search("15/01/2019T04:05:06.789120") is None + + tz_zz_re = parser.DateTimeParser._TZ_ZZ_RE + assert tz_zz_re.findall("-07:00") == [("-", "07", "00")] + assert tz_zz_re.findall("+07") == [("+", "07", "")] + assert tz_zz_re.search("15/01/2019T04:05:06.789120Z") is not None + assert tz_zz_re.search("15/01/2019T04:05:06.789120") is None + + tz_name_re = parser.DateTimeParser._TZ_NAME_RE + assert tz_name_re.findall("Europe/Warsaw") == ["Europe/Warsaw"] + assert tz_name_re.findall("GMT") == ["GMT"] + + def test_timestamp(self): + timestamp_re = parser.DateTimeParser._TIMESTAMP_RE + assert timestamp_re.findall("1565707550.452729") == ["1565707550.452729"] + assert timestamp_re.findall("-1565707550.452729") == ["-1565707550.452729"] + assert timestamp_re.findall("-1565707550") == ["-1565707550"] + assert timestamp_re.findall("1565707550") == ["1565707550"] + assert timestamp_re.findall("1565707550.") == [] + assert timestamp_re.findall(".1565707550") == [] + + def test_timestamp_milli(self): + timestamp_expanded_re = parser.DateTimeParser._TIMESTAMP_EXPANDED_RE + assert timestamp_expanded_re.findall("-1565707550") == ["-1565707550"] + assert timestamp_expanded_re.findall("1565707550") == ["1565707550"] + assert timestamp_expanded_re.findall("1565707550.452729") == [] + assert timestamp_expanded_re.findall("1565707550.") == [] + assert timestamp_expanded_re.findall(".1565707550") == [] + + def test_time(self): + time_re = parser.DateTimeParser._TIME_RE + time_seperators = [":", ""] + + for sep in time_seperators: + assert time_re.findall("12") == [("12", "", "", "", "")] + assert time_re.findall("12{sep}35".format(sep=sep)) == [ + ("12", "35", "", "", "") + ] + assert time_re.findall("12{sep}35{sep}46".format(sep=sep)) == [ + ("12", "35", "46", "", "") + ] + assert time_re.findall("12{sep}35{sep}46.952313".format(sep=sep)) == [ + ("12", "35", "46", ".", "952313") + ] + assert time_re.findall("12{sep}35{sep}46,952313".format(sep=sep)) == [ + ("12", "35", "46", ",", "952313") + ] + + assert time_re.findall("12:") == [] + assert time_re.findall("12:35:46.") == [] + assert time_re.findall("12:35:46,") == [] + + +class TestDateTimeParserISO: + @classmethod + def setup_class(cls): + cls.parser = parser.DateTimeParser("en_us") + + def test_YYYY(self): + + assert self.parser.parse_iso("2013") == datetime(2013, 1, 1) + + def test_YYYY_DDDD(self): + assert self.parser.parse_iso("1998-136") == datetime(1998, 5, 16) + + assert self.parser.parse_iso("1998-006") == datetime(1998, 1, 6) + + with pytest.raises(ParserError): + self.parser.parse_iso("1998-456") + + # 2016 is a leap year, so Feb 29 exists (leap day) + assert self.parser.parse_iso("2016-059") == datetime(2016, 2, 28) + assert self.parser.parse_iso("2016-060") == datetime(2016, 2, 29) + assert self.parser.parse_iso("2016-061") == datetime(2016, 3, 1) + + # 2017 is not a leap year, so Feb 29 does not exist + assert self.parser.parse_iso("2017-059") == datetime(2017, 2, 28) + assert self.parser.parse_iso("2017-060") == datetime(2017, 3, 1) + assert self.parser.parse_iso("2017-061") == datetime(2017, 3, 2) + + # Since 2016 is a leap year, the 366th day falls in the same year + assert self.parser.parse_iso("2016-366") == datetime(2016, 12, 31) + + # Since 2017 is not a leap year, the 366th day falls in the next year + 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) + ) + + assert self.parser.parse_iso("2013-036 04:05:06Z") == datetime( + 2013, 2, 5, 4, 5, 6, tzinfo=tz.tzutc() + ) + + def test_YYYY_MM_DDDD(self): + with pytest.raises(ParserError): + 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") + + with pytest.raises(ParserError): + self.parser.parse_iso("2013-02-03 044") + + with pytest.raises(ParserError): + 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 + ) + + assert self.parser.parse_iso("2013-02-03T04:05:06.78") == datetime( + 2013, 2, 3, 4, 5, 6, 780000 + ) + + assert self.parser.parse_iso("2013-02-03T04:05:06.789") == datetime( + 2013, 2, 3, 4, 5, 6, 789000 + ) + + assert self.parser.parse_iso("2013-02-03T04:05:06.7891") == datetime( + 2013, 2, 3, 4, 5, 6, 789100 + ) + + assert self.parser.parse_iso("2013-02-03T04:05:06.78912") == datetime( + 2013, 2, 3, 4, 5, 6, 789120 + ) + + # ISO 8601:2004(E), ISO, 2004-12-01, 4.2.2.4 ... the decimal fraction + # shall be divided from the integer part by the decimal sign specified + # in ISO 31-0, i.e. the comma [,] or full stop [.]. Of these, the comma + # is the preferred sign. + assert self.parser.parse_iso("2013-02-03T04:05:06,789123678") == datetime( + 2013, 2, 3, 4, 5, 6, 789124 + ) + + # there is no limit on the number of decimal places + assert self.parser.parse_iso("2013-02-03T04:05:06.789123678") == datetime( + 2013, 2, 3, 4, 5, 6, 789124 + ) + + 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) + ) + + assert self.parser.parse_iso("2013-02-03T04:05:06.78+01:00") == datetime( + 2013, 2, 3, 4, 5, 6, 780000, tzinfo=tz.tzoffset(None, 3600) + ) + + assert self.parser.parse_iso("2013-02-03T04:05:06.789+01:00") == datetime( + 2013, 2, 3, 4, 5, 6, 789000, tzinfo=tz.tzoffset(None, 3600) + ) + + assert self.parser.parse_iso("2013-02-03T04:05:06.7891+01:00") == datetime( + 2013, 2, 3, 4, 5, 6, 789100, tzinfo=tz.tzoffset(None, 3600) + ) + + assert self.parser.parse_iso("2013-02-03T04:05:06.78912+01:00") == datetime( + 2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzoffset(None, 3600) + ) + + assert self.parser.parse_iso("2013-02-03 04:05:06.78912Z") == datetime( + 2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzutc() + ) + + def test_invalid_Z(self): + + with pytest.raises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912z") + + with pytest.raises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912zz") + + with pytest.raises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912Zz") + + with pytest.raises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912ZZ") + + with pytest.raises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912+Z") + + with pytest.raises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912-Z") + + with pytest.raises(ParserError): + self.parser.parse_iso("2013-02-03T04:05:06.78912 Z") + + def test_parse_subsecond(self): + self.expected = datetime(2013, 1, 1, 12, 30, 45, 900000) + assert self.parser.parse_iso("2013-01-01 12:30:45.9") == self.expected + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 980000) + assert self.parser.parse_iso("2013-01-01 12:30:45.98") == self.expected + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987000) + assert self.parser.parse_iso("2013-01-01 12:30:45.987") == self.expected + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987600) + assert self.parser.parse_iso("2013-01-01 12:30:45.9876") == self.expected + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987650) + assert self.parser.parse_iso("2013-01-01 12:30:45.98765") == self.expected + + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) + assert self.parser.parse_iso("2013-01-01 12:30:45.987654") == self.expected + + # use comma as subsecond separator + self.expected = datetime(2013, 1, 1, 12, 30, 45, 987654) + assert self.parser.parse_iso("2013-01-01 12:30:45,987654") == self.expected + + def test_gnu_date(self): + """Regression tests for parsing output from GNU date.""" + # date -Ins + assert self.parser.parse_iso("2016-11-16T09:46:30,895636557-0800") == datetime( + 2016, 11, 16, 9, 46, 30, 895636, tzinfo=tz.tzoffset(None, -3600 * 8) + ) + + # date --rfc-3339=ns + assert self.parser.parse_iso("2016-11-16 09:51:14.682141526-08:00") == datetime( + 2016, 11, 16, 9, 51, 14, 682142, tzinfo=tz.tzoffset(None, -3600 * 8) + ) + + def test_isoformat(self): + + dt = datetime.utcnow() + + assert self.parser.parse_iso(dt.isoformat()) == dt + + def test_parse_iso_with_leading_and_trailing_whitespace(self): + datetime_string = " 2016-11-15T06:37:19.123456" + with pytest.raises(ParserError): + self.parser.parse_iso(datetime_string) + + datetime_string = " 2016-11-15T06:37:19.123456 " + with pytest.raises(ParserError): + self.parser.parse_iso(datetime_string) + + datetime_string = "2016-11-15T06:37:19.123456 " + with pytest.raises(ParserError): + self.parser.parse_iso(datetime_string) + + datetime_string = "2016-11-15T 06:37:19.123456" + with pytest.raises(ParserError): + self.parser.parse_iso(datetime_string) + + # leading whitespace + datetime_string = " 2016-11-15 06:37:19.123456" + with pytest.raises(ParserError): + self.parser.parse_iso(datetime_string) + + # trailing whitespace + datetime_string = "2016-11-15 06:37:19.123456 " + with pytest.raises(ParserError): + self.parser.parse_iso(datetime_string) + + datetime_string = " 2016-11-15 06:37:19.123456 " + with pytest.raises(ParserError): + self.parser.parse_iso(datetime_string) + + # two dividing spaces + datetime_string = "2016-11-15 06:37:19.123456" + with pytest.raises(ParserError): + self.parser.parse_iso(datetime_string) + + def test_parse_iso_with_extra_words_at_start_and_end_invalid(self): + test_inputs = [ + "blah2016", + "blah2016blah", + "blah 2016 blah", + "blah 2016", + "2016 blah", + "blah 2016-05-16 04:05:06.789120", + "2016-05-16 04:05:06.789120 blah", + "blah 2016-05-16T04:05:06.789120", + "2016-05-16T04:05:06.789120 blah", + "2016blah", + "2016-05blah", + "2016-05-16blah", + "2016-05-16T04:05:06.789120blah", + "2016-05-16T04:05:06.789120ZblahZ", + "2016-05-16T04:05:06.789120Zblah", + "2016-05-16T04:05:06.789120blahZ", + "Meet me at 2016-05-16T04:05:06.789120 at the restaurant.", + "Meet me at 2016-05-16 04:05:06.789120 at the restaurant.", + ] + + for ti in test_inputs: + with pytest.raises(ParserError): + self.parser.parse_iso(ti) + + def test_iso8601_basic_format(self): + assert self.parser.parse_iso("20180517") == datetime(2018, 5, 17) + + assert self.parser.parse_iso("20180517T10") == datetime(2018, 5, 17, 10) + + assert self.parser.parse_iso("20180517T105513.843456") == datetime( + 2018, 5, 17, 10, 55, 13, 843456 + ) + + assert self.parser.parse_iso("20180517T105513Z") == datetime( + 2018, 5, 17, 10, 55, 13, tzinfo=tz.tzutc() + ) + + assert self.parser.parse_iso("20180517T105513.843456-0700") == datetime( + 2018, 5, 17, 10, 55, 13, 843456, tzinfo=tz.tzoffset(None, -25200) + ) + + assert self.parser.parse_iso("20180517T105513-0700") == datetime( + 2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200) + ) + + assert self.parser.parse_iso("20180517T105513-07") == datetime( + 2018, 5, 17, 10, 55, 13, tzinfo=tz.tzoffset(None, -25200) + ) + + # ordinal in basic format: YYYYDDDD + assert self.parser.parse_iso("1998136") == datetime(1998, 5, 16) + + # timezone requires +- seperator + with pytest.raises(ParserError): + self.parser.parse_iso("20180517T1055130700") + + with pytest.raises(ParserError): + self.parser.parse_iso("20180517T10551307") + + # too many digits in date + with pytest.raises(ParserError): + self.parser.parse_iso("201860517T105513Z") + + # too many digits in time + with pytest.raises(ParserError): + self.parser.parse_iso("20180517T1055213Z") + + def test_midnight_end_day(self): + assert self.parser.parse_iso("2019-10-30T24:00:00") == datetime( + 2019, 10, 31, 0, 0, 0, 0 + ) + assert self.parser.parse_iso("2019-10-30T24:00") == datetime( + 2019, 10, 31, 0, 0, 0, 0 + ) + assert self.parser.parse_iso("2019-10-30T24:00:00.0") == datetime( + 2019, 10, 31, 0, 0, 0, 0 + ) + assert self.parser.parse_iso("2019-10-31T24:00:00") == datetime( + 2019, 11, 1, 0, 0, 0, 0 + ) + assert self.parser.parse_iso("2019-12-31T24:00:00") == datetime( + 2020, 1, 1, 0, 0, 0, 0 + ) + assert self.parser.parse_iso("2019-12-31T23:59:59.9999999") == datetime( + 2020, 1, 1, 0, 0, 0, 0 + ) + + with pytest.raises(ParserError): + self.parser.parse_iso("2019-12-31T24:01:00") + + with pytest.raises(ParserError): + self.parser.parse_iso("2019-12-31T24:00:01") + + with pytest.raises(ParserError): + self.parser.parse_iso("2019-12-31T24:00:00.1") + + with pytest.raises(ParserError): + self.parser.parse_iso("2019-12-31T24:00:00.999999") + + +class TestTzinfoParser: + @classmethod + def setup_class(cls): + cls.parser = parser.TzinfoParser() + + 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() + + 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("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("01") == tz.tzoffset(None, 3600) + assert self.parser.parse("+01") == tz.tzoffset(None, 3600) + 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") + + +class TestDateTimeParserMonthName: + @classmethod + def setup_class(cls): + cls.parser = parser.DateTimeParser("en_us") + + 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 + ) + + def test_localized_month_name(self): + parser_ = parser.DateTimeParser("fr_fr") + + assert parser_.parse("2013-Janvier-01", "YYYY-MMMM-DD") == datetime(2013, 1, 1) + + def test_localized_month_abbreviation(self): + parser_ = parser.DateTimeParser("it_it") + + assert parser_.parse("2013-Gen-01", "YYYY-MMM-DD") == datetime(2013, 1, 1) + + +class TestDateTimeParserMeridians: + @classmethod + def setup_class(cls): + cls.parser = parser.DateTimeParser("en_us") + + def test_meridians_lowercase(self): + assert self.parser.parse("2013-01-01 5am", "YYYY-MM-DD ha") == datetime( + 2013, 1, 1, 5 + ) + + assert self.parser.parse("2013-01-01 5pm", "YYYY-MM-DD ha") == datetime( + 2013, 1, 1, 17 + ) + + def test_meridians_capitalized(self): + assert self.parser.parse("2013-01-01 5AM", "YYYY-MM-DD hA") == datetime( + 2013, 1, 1, 5 + ) + + assert self.parser.parse("2013-01-01 5PM", "YYYY-MM-DD hA") == datetime( + 2013, 1, 1, 17 + ) + + def test_localized_meridians_lowercase(self): + parser_ = parser.DateTimeParser("hu_hu") + assert parser_.parse("2013-01-01 5 de", "YYYY-MM-DD h a") == datetime( + 2013, 1, 1, 5 + ) + + assert parser_.parse("2013-01-01 5 du", "YYYY-MM-DD h a") == datetime( + 2013, 1, 1, 17 + ) + + def test_localized_meridians_capitalized(self): + parser_ = parser.DateTimeParser("hu_hu") + assert parser_.parse("2013-01-01 5 DE", "YYYY-MM-DD h A") == datetime( + 2013, 1, 1, 5 + ) + + assert parser_.parse("2013-01-01 5 DU", "YYYY-MM-DD h A") == datetime( + 2013, 1, 1, 17 + ) + + # regression test for issue #607 + def test_es_meridians(self): + parser_ = parser.DateTimeParser("es") + + assert parser_.parse( + "Junio 30, 2019 - 08:00 pm", "MMMM DD, YYYY - hh:mm a" + ) == datetime(2019, 6, 30, 20, 0) + + with pytest.raises(ParserError): + parser_.parse( + "Junio 30, 2019 - 08:00 pasdfasdfm", "MMMM DD, YYYY - hh:mm a" + ) + + def test_fr_meridians(self): + parser_ = parser.DateTimeParser("fr") + + # the French locale always uses a 24 hour clock, so it does not support meridians + with pytest.raises(ParserError): + parser_.parse("Janvier 30, 2019 - 08:00 pm", "MMMM DD, YYYY - hh:mm a") + + +class TestDateTimeParserMonthOrdinalDay: + @classmethod + def setup_class(cls): + cls.parser = parser.DateTimeParser("en_us") + + def test_english(self): + parser_ = parser.DateTimeParser("en_us") + + assert parser_.parse("January 1st, 2013", "MMMM Do, YYYY") == datetime( + 2013, 1, 1 + ) + assert parser_.parse("January 2nd, 2013", "MMMM Do, YYYY") == datetime( + 2013, 1, 2 + ) + assert parser_.parse("January 3rd, 2013", "MMMM Do, YYYY") == datetime( + 2013, 1, 3 + ) + assert parser_.parse("January 4th, 2013", "MMMM Do, YYYY") == datetime( + 2013, 1, 4 + ) + assert parser_.parse("January 11th, 2013", "MMMM Do, YYYY") == datetime( + 2013, 1, 11 + ) + assert parser_.parse("January 12th, 2013", "MMMM Do, YYYY") == datetime( + 2013, 1, 12 + ) + assert parser_.parse("January 13th, 2013", "MMMM Do, YYYY") == datetime( + 2013, 1, 13 + ) + assert parser_.parse("January 21st, 2013", "MMMM Do, YYYY") == datetime( + 2013, 1, 21 + ) + assert parser_.parse("January 31st, 2013", "MMMM Do, YYYY") == datetime( + 2013, 1, 31 + ) + + with pytest.raises(ParserError): + parser_.parse("January 1th, 2013", "MMMM Do, YYYY") + + with pytest.raises(ParserError): + parser_.parse("January 11st, 2013", "MMMM Do, YYYY") + + def test_italian(self): + parser_ = parser.DateTimeParser("it_it") + + assert parser_.parse("Gennaio 1º, 2013", "MMMM Do, YYYY") == datetime( + 2013, 1, 1 + ) + + def test_spanish(self): + parser_ = parser.DateTimeParser("es_es") + + assert parser_.parse("Enero 1º, 2013", "MMMM Do, YYYY") == datetime(2013, 1, 1) + + def test_french(self): + parser_ = parser.DateTimeParser("fr_fr") + + assert parser_.parse("Janvier 1er, 2013", "MMMM Do, YYYY") == datetime( + 2013, 1, 1 + ) + + assert parser_.parse("Janvier 2e, 2013", "MMMM Do, YYYY") == datetime( + 2013, 1, 2 + ) + + assert parser_.parse("Janvier 11e, 2013", "MMMM Do, YYYY") == datetime( + 2013, 1, 11 + ) + + +class TestDateTimeParserSearchDate: + @classmethod + def setup_class(cls): + cls.parser = parser.DateTimeParser() + + 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) + + assert self.parser.parse( + "Call 01-02-03 on 79-01-01 12:05:10", "YY-MM-DD HH:mm:ss" + ) == 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 + ) + + def test_parse_search_locale_with_names(self): + p = parser.DateTimeParser("sv_se") + + assert p.parse("Jan föddes den 31 Dec 1980", "DD MMM YYYY") == datetime( + 1980, 12, 31 + ) + + assert p.parse("Jag föddes den 25 Augusti 1975", "DD MMMM YYYY") == datetime( + 1975, 8, 25 + ) + + 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 + ) == datetime(2015, 12, 10, 17, 9) + + format = "[MMMM] M D, YYYY [at] h:mma" + assert self.parser.parse("MMMM 12 10, 2015 at 5:09pm", format) == datetime( + 2015, 12, 10, 17, 9 + ) + + format = "[It happened on] MMMM Do [in the year] YYYY [a long time ago]" + assert self.parser.parse( + "It happened on November 25th in the year 1990 a long time ago", format + ) == datetime(1990, 11, 25) + + format = "[It happened on] MMMM Do [in the][ year] YYYY [a long time ago]" + assert self.parser.parse( + "It happened on November 25th in the year 1990 a long time ago", format + ) == datetime(1990, 11, 25) + + format = "[I'm][ entirely][ escaped,][ weee!]" + assert self.parser.parse("I'm entirely escaped, weee!", format) == datetime( + 1, 1, 1 + ) + + # Special RegEx characters + format = "MMM DD, YYYY |^${}().*+?<>-& h:mm A" + assert self.parser.parse( + "Dec 31, 2017 |^${}().*+?<>-& 2:00 AM", format + ) == datetime(2017, 12, 31, 2, 0) diff --git a/tests/util_tests.py b/tests/test_util.py similarity index 99% rename from tests/util_tests.py rename to tests/test_util.py index b8a2c1360..7a0b974de 100644 --- a/tests/util_tests.py +++ b/tests/test_util.py @@ -2,9 +2,10 @@ import time from datetime import datetime -from arrow import util import pytest +from arrow import util + class TestUtil: def test_total_seconds(self): diff --git a/tests/util_tests.py.bak b/tests/util_tests.py.bak deleted file mode 100644 index 804ae62a2..000000000 --- a/tests/util_tests.py.bak +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -import time -from datetime import datetime - -from chai import Chai - -from arrow import util - - -class UtilTests(Chai): - def test_total_seconds(self): - td = datetime(2019, 1, 1) - datetime(2018, 1, 1) - self.assertEqual(util.total_seconds(td), td.total_seconds()) - - def test_is_timestamp(self): - timestamp_float = time.time() - timestamp_int = int(timestamp_float) - - self.assertTrue(util.is_timestamp(timestamp_int)) - self.assertTrue(util.is_timestamp(timestamp_float)) - self.assertTrue(util.is_timestamp(str(timestamp_int))) - self.assertTrue(util.is_timestamp(str(timestamp_float))) - - self.assertFalse(util.is_timestamp(True)) - self.assertFalse(util.is_timestamp(False)) - - class InvalidTimestamp: - pass - - self.assertFalse(util.is_timestamp(InvalidTimestamp())) - - full_datetime = "2019-06-23T13:12:42" - self.assertFalse(util.is_timestamp(full_datetime)) - - def test_iso_gregorian(self): - with self.assertRaises(ValueError): - util.iso_to_gregorian(2013, 0, 5) - - with self.assertRaises(ValueError): - util.iso_to_gregorian(2013, 8, 0) diff --git a/tests/utils.py b/tests/utils.py index bb469b165..2a048feb3 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,8 +2,15 @@ import pytz from dateutil.zoneinfo import get_zonefile_instance +from arrow import util + def make_full_tz_list(): dateutil_zones = set(get_zonefile_instance().zones) pytz_zones = set(pytz.all_timezones) return dateutil_zones.union(pytz_zones) + + +def assert_datetime_equality(dt1, dt2, within=10): + assert dt1.tzinfo == dt2.tzinfo + assert abs(util.total_seconds(dt1 - dt2)) < within diff --git a/tox.ini b/tox.ini index 061f39261..071edf7c3 100644 --- a/tox.ini +++ b/tox.ini @@ -4,8 +4,8 @@ skip_missing_interpreters = true [testenv] deps = -rrequirements.txt -whitelist_externals = nosetests -commands = nosetests +whitelist_externals = pytest +commands = pytest [testenv:lint] basepython = python3 From f664c139c140824fd9dde650f9eade7d1af4be4d Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 4 Jan 2020 17:11:10 -0500 Subject: [PATCH 374/649] Remove test suite support from setup.py as it will soon be deprecated --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index 8778bebf1..a60250a03 100644 --- a/setup.py +++ b/setup.py @@ -27,8 +27,6 @@ "python-dateutil", "backports.functools_lru_cache>=1.2.1;python_version=='2.7'", ], - test_suite="tests", - tests_require=["chai", "mock"], classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", From 3d667ed14d85bfd2a7b56b52f399fcb189a4d76d Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 4 Jan 2020 17:37:45 -0500 Subject: [PATCH 375/649] Added Pytest options to setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 1f7575065..227bb8b65 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [tool:pytest] -# addopts = -v --nf --cov-branch --cov=arrow tests --cov-fail-under=100 --cov-report=term-missing --cov-report=xml +addopts = -v --nf --cov-branch --cov=arrow tests --cov-fail-under=100 --cov-report=term-missing --cov-report=xml [tool:isort] line_length = 88 From 0e2f45bceb6db321c5832e6797baf3041908bba8 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 5 Jan 2020 16:22:51 -0500 Subject: [PATCH 376/649] Added coverage.xml to make clean command --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a05957d4a..f15a106ae 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ docs: clean: rm -rf venv .tox ./**/__pycache__ rm -rf dist build .egg .eggs arrow.egg-info - rm -f ./**/*.pyc .coverage + rm -f ./**/*.pyc .coverage coverage.xml publish: rm -rf dist build .egg .eggs arrow.egg-info From 82fe11f47e4934390afbc4109d0769dbb1204e70 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 6 Jan 2020 14:14:45 -0500 Subject: [PATCH 377/649] Remove new first --nf argument from pytest --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 227bb8b65..f8ea7e011 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [tool:pytest] -addopts = -v --nf --cov-branch --cov=arrow tests --cov-fail-under=100 --cov-report=term-missing --cov-report=xml +addopts = -v --cov-branch --cov=arrow tests --cov-fail-under=100 --cov-report=term-missing --cov-report=xml [tool:isort] line_length = 88 From 0ee8d5021d932f6ee798248f3a7ae695d63a2a4c Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Wed, 8 Jan 2020 16:33:46 -0500 Subject: [PATCH 378/649] Locked dependency versions --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 11c0c72cf..79f0e53ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ backports.functools_lru_cache==1.6.1; python_version == "2.7" dateparser==0.7.* pre-commit==1.20.* -pytest -pytest-cov -pytest-mock +pytest==5.3.* +pytest-cov==2.8.* +pytest-mock==2.0.* python-dateutil==2.8.* pytz==2019.* simplejson==3.16.* From c1ca3b037b730207c6d8583b12626787c0879e55 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 9 Jan 2020 08:27:54 -0500 Subject: [PATCH 379/649] Lock pytest for specific Python versions. --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 79f0e53ad..e1d16ca9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ backports.functools_lru_cache==1.6.1; python_version == "2.7" dateparser==0.7.* pre-commit==1.20.* -pytest==5.3.* +pytest==4.6.*; python_version == "2.7" +pytest==5.3.*; python_version >= "3.5" pytest-cov==2.8.* pytest-mock==2.0.* python-dateutil==2.8.* From 1381a0e84cbb5c1d42755bc95628837b1b512be1 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 11 Jan 2020 18:43:28 -0500 Subject: [PATCH 380/649] Force pip3 travis --- .travis.yml | 76 ++++++++++++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/.travis.yml b/.travis.yml index e549ef4d4..0e02e875b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,43 +1,43 @@ language: python dist: bionic matrix: - include: - - name: "Python 2.7" - python: "2.7" - env: TOXENV=py27 - - name: "Python 3.5" - dist: xenial - python: "3.5" - env: TOXENV=py35 - - name: "Python 3.6" - python: "3.6" - env: TOXENV=py36 - - name: "Python 3.7" - python: "3.7" - env: TOXENV=py37 - - name: "Python 3.7 on macOS" - os: osx - osx_image: xcode11 - language: shell # 'language: python' is an error on Travis CI macOS - env: TOXENV=py37 - - name: "Python 3.7 on Windows" - os: windows - language: shell # 'language: python' is an error on Travis CI Windows - before_install: - - choco install python --version 3.7.5 - - python -m pip install --upgrade pip - env: - - PATH=/c/Python37:/c/Python37/Scripts:$PATH - - TOXENV=py37 - - name: "Python 3.8" - python: "3.8" - env: TOXENV=py38 - - name: "Linting" - python: "3.7" - env: TOXENV=lint,docs - cache: - directories: - - $HOME/.cache/pre-commit -install: pip install -U codecov tox + include: + - name: "Python 2.7" + python: "2.7" + env: TOXENV=py27 + - name: "Python 3.5" + dist: xenial + python: "3.5" + env: TOXENV=py35 + - name: "Python 3.6" + python: "3.6" + env: TOXENV=py36 + - name: "Python 3.7" + python: "3.7" + env: TOXENV=py37 + - name: "Python 3.7 on macOS" + os: osx + osx_image: xcode11 + language: shell # 'language: python' is an error on Travis CI macOS + env: TOXENV=py37 + - name: "Python 3.7 on Windows" + os: windows + language: shell # 'language: python' is an error on Travis CI Windows + before_install: + - choco install python --version 3.7.5 + - python -m pip install --upgrade pip + env: + - PATH=/c/Python37:/c/Python37/Scripts:$PATH + - TOXENV=py37 + - name: "Python 3.8" + python: "3.8" + env: TOXENV=py38 + - name: "Linting" + python: "3.7" + env: TOXENV=lint,docs + cache: + directories: + - $HOME/.cache/pre-commit +install: pip3 install -U codecov tox script: tox after_success: codecov From 4a56296074b080b38258fa58b697742109289ed6 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 11 Jan 2020 18:49:00 -0500 Subject: [PATCH 381/649] Test pip3 vs pip --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0e02e875b..44ddf7a25 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,6 +38,7 @@ matrix: cache: directories: - $HOME/.cache/pre-commit -install: pip3 install -U codecov tox +before_install: pip3 install --upgrade pip || pip install --upgrade pip +install: pip3 install -U codecov tox || pip install -U codecov tox script: tox after_success: codecov From 6aa8b82fa33bd80998e23db588cd34fc9d1392f3 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 11 Jan 2020 18:55:32 -0500 Subject: [PATCH 382/649] Tweak Python 2.7 targets --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 44ddf7a25..d32210164 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,8 @@ matrix: - name: "Python 2.7" python: "2.7" env: TOXENV=py27 + before_install: pip install --upgrade pip + install: pip install -U codecov tox - name: "Python 3.5" dist: xenial python: "3.5" @@ -38,7 +40,7 @@ matrix: cache: directories: - $HOME/.cache/pre-commit -before_install: pip3 install --upgrade pip || pip install --upgrade pip -install: pip3 install -U codecov tox || pip install -U codecov tox +before_install: pip3 install --upgrade pip +install: pip3 install -U codecov tox script: tox after_success: codecov From 3298ccd331720a355561037450828397195cc684 Mon Sep 17 00:00:00 2001 From: Giovanni Cannata Date: Tue, 18 Feb 2020 11:33:54 +0100 Subject: [PATCH 383/649] Update locales.py Italian locale is missing translation for "week" and "weeks" --- arrow/locales.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index a42947f66..d850f3e0a 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -331,6 +331,8 @@ class ItalianLocale(Locale): "hours": "{0} ore", "day": "un giorno", "days": "{0} giorni", + "week": "una settimana,", + "weeks": "{0} settimane", "month": "un mese", "months": "{0} mesi", "year": "un anno", From cbcf6d0baa98f82ff6de46817fb147865c53fcd6 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 3 Mar 2020 13:18:24 -0500 Subject: [PATCH 384/649] Added sponsorship info and update dependencies (#759) * Added sponsorship info and update dependencies * Fix python 2.7 and 3.5 pre-commit * Up pre-commit dependencies and remove pre-commit cache --- .github/FUNDING.yml | 1 + .pre-commit-config.yaml | 8 ++++---- .travis.yml | 3 --- requirements.txt | 7 ++++--- 4 files changed, 9 insertions(+), 10 deletions(-) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..c3608357a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +open_collective: arrow diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4abab574e..ea8b19f8c 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: v2.4.0 + rev: v2.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -16,7 +16,7 @@ repos: - id: check-merge-conflict - id: debug-statements - repo: https://github.com/asottile/seed-isort-config - rev: v1.9.3 + rev: v2.1.0 hooks: - id: seed-isort-config - repo: https://github.com/pre-commit/mirrors-isort @@ -24,11 +24,11 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v1.25.1 + rev: v2.1.0 hooks: - id: pyupgrade - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.4.2 + rev: v1.5.1 hooks: - id: python-no-eval - id: python-check-blanket-noqa diff --git a/.travis.yml b/.travis.yml index d32210164..688c7ecfd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,9 +37,6 @@ matrix: - name: "Linting" python: "3.7" env: TOXENV=lint,docs - cache: - directories: - - $HOME/.cache/pre-commit before_install: pip3 install --upgrade pip install: pip3 install -U codecov tox script: tox diff --git a/requirements.txt b/requirements.txt index e1d16ca9a..dd2046d68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,13 @@ backports.functools_lru_cache==1.6.1; python_version == "2.7" dateparser==0.7.* -pre-commit==1.20.* +pre-commit==1.20.*; python_version <= "3.5" +pre-commit==2.1.*; python_version >= "3.6" pytest==4.6.*; python_version == "2.7" pytest==5.3.*; python_version >= "3.5" pytest-cov==2.8.* pytest-mock==2.0.* python-dateutil==2.8.* pytz==2019.* -simplejson==3.16.* +simplejson==3.17.* sphinx==1.8.*; python_version == "2.7" -sphinx==2.2.*; python_version >= "3.5" +sphinx==2.4.*; python_version >= "3.5" From 750c82d1ee78f845233295d91a129687f3351aaa Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 3 Mar 2020 14:00:36 -0500 Subject: [PATCH 385/649] First attempt at GH actions (#761) --- .github/WORKFLOWS/continuous_integration.yml | 105 +++++++++++++++++++ .travis.yml | 43 -------- Makefile | 6 +- setup.cfg | 2 +- 4 files changed, 109 insertions(+), 47 deletions(-) create mode 100644 .github/WORKFLOWS/continuous_integration.yml delete mode 100644 .travis.yml diff --git a/.github/WORKFLOWS/continuous_integration.yml b/.github/WORKFLOWS/continuous_integration.yml new file mode 100644 index 000000000..44c489d00 --- /dev/null +++ b/.github/WORKFLOWS/continuous_integration.yml @@ -0,0 +1,105 @@ +name: tests + +on: + push: + branches: + - master + pull_request: + branches: + - master + schedule: + - cron: "0 0 1 * *" # run tests monthly + +jobs: + Linting: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + - name: Linting code + run: tox -e lint + - name: Linting docs + run: tox -e docs + + Linux: + needs: Linting + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Test with tox + run: tox + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: .coverage + + MacOS: + needs: Linting + runs-on: macos-latest + strategy: + matrix: + python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Test with tox + run: tox + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: .coverage + + Windows: + needs: Linting + runs-on: windows-latest + strategy: + matrix: + python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Test with tox + run: tox + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: .coverage diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 688c7ecfd..000000000 --- a/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -language: python -dist: bionic -matrix: - include: - - name: "Python 2.7" - python: "2.7" - env: TOXENV=py27 - before_install: pip install --upgrade pip - install: pip install -U codecov tox - - name: "Python 3.5" - dist: xenial - python: "3.5" - env: TOXENV=py35 - - name: "Python 3.6" - python: "3.6" - env: TOXENV=py36 - - name: "Python 3.7" - python: "3.7" - env: TOXENV=py37 - - name: "Python 3.7 on macOS" - os: osx - osx_image: xcode11 - language: shell # 'language: python' is an error on Travis CI macOS - env: TOXENV=py37 - - name: "Python 3.7 on Windows" - os: windows - language: shell # 'language: python' is an error on Travis CI Windows - before_install: - - choco install python --version 3.7.5 - - python -m pip install --upgrade pip - env: - - PATH=/c/Python37:/c/Python37/Scripts:$PATH - - TOXENV=py37 - - name: "Python 3.8" - python: "3.8" - env: TOXENV=py38 - - name: "Linting" - python: "3.7" - env: TOXENV=lint,docs -before_install: pip3 install --upgrade pip -install: pip3 install -U codecov tox -script: tox -after_success: codecov diff --git a/Makefile b/Makefile index f15a106ae..7c7f7379a 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: auto test docs clean -auto: build27 +auto: build38 build27: PYTHON_VER = python2.7 build35: PYTHON_VER = python3.5 @@ -14,7 +14,7 @@ build27 build35 build36 build37 build38: venv/bin/pre-commit install test: - rm -f .coverage coverage.xml + rm -f .coverage . venv/bin/activate && pytest lint: @@ -27,7 +27,7 @@ docs: clean: rm -rf venv .tox ./**/__pycache__ rm -rf dist build .egg .eggs arrow.egg-info - rm -f ./**/*.pyc .coverage coverage.xml + rm -f ./**/*.pyc .coverage publish: rm -rf dist build .egg .eggs arrow.egg-info diff --git a/setup.cfg b/setup.cfg index f8ea7e011..039cd09b8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [tool:pytest] -addopts = -v --cov-branch --cov=arrow tests --cov-fail-under=100 --cov-report=term-missing --cov-report=xml +addopts = -v --cov-branch --cov=arrow tests --cov-fail-under=100 --cov-report=term-missing [tool:isort] line_length = 88 From 7b348dff58adbff1c7190cd473d6f3c28fb072ec Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 3 Mar 2020 14:07:16 -0500 Subject: [PATCH 386/649] Temporarily restore travis CI and rename workflows folder to lowercase --- .github/workflows/continuous_integration.yml | 105 +++++++++++++++++++ .travis.yml | 43 ++++++++ 2 files changed, 148 insertions(+) create mode 100644 .github/workflows/continuous_integration.yml create mode 100644 .travis.yml diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml new file mode 100644 index 000000000..44c489d00 --- /dev/null +++ b/.github/workflows/continuous_integration.yml @@ -0,0 +1,105 @@ +name: tests + +on: + push: + branches: + - master + pull_request: + branches: + - master + schedule: + - cron: "0 0 1 * *" # run tests monthly + +jobs: + Linting: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + - name: Linting code + run: tox -e lint + - name: Linting docs + run: tox -e docs + + Linux: + needs: Linting + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Test with tox + run: tox + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: .coverage + + MacOS: + needs: Linting + runs-on: macos-latest + strategy: + matrix: + python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Test with tox + run: tox + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: .coverage + + Windows: + needs: Linting + runs-on: windows-latest + strategy: + matrix: + python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Test with tox + run: tox + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: .coverage diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..688c7ecfd --- /dev/null +++ b/.travis.yml @@ -0,0 +1,43 @@ +language: python +dist: bionic +matrix: + include: + - name: "Python 2.7" + python: "2.7" + env: TOXENV=py27 + before_install: pip install --upgrade pip + install: pip install -U codecov tox + - name: "Python 3.5" + dist: xenial + python: "3.5" + env: TOXENV=py35 + - name: "Python 3.6" + python: "3.6" + env: TOXENV=py36 + - name: "Python 3.7" + python: "3.7" + env: TOXENV=py37 + - name: "Python 3.7 on macOS" + os: osx + osx_image: xcode11 + language: shell # 'language: python' is an error on Travis CI macOS + env: TOXENV=py37 + - name: "Python 3.7 on Windows" + os: windows + language: shell # 'language: python' is an error on Travis CI Windows + before_install: + - choco install python --version 3.7.5 + - python -m pip install --upgrade pip + env: + - PATH=/c/Python37:/c/Python37/Scripts:$PATH + - TOXENV=py37 + - name: "Python 3.8" + python: "3.8" + env: TOXENV=py38 + - name: "Linting" + python: "3.7" + env: TOXENV=lint,docs +before_install: pip3 install --upgrade pip +install: pip3 install -U codecov tox +script: tox +after_success: codecov From bd37b7f3ff4c3afb3a87e238ed093adf1e045f42 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 3 Mar 2020 14:11:33 -0500 Subject: [PATCH 387/649] Fix naming conflict and removed linting need on tests --- .github/WORKFLOWS/continuous_integration.yml | 105 ------------------- .github/workflows/continuous_integration.yml | 3 - 2 files changed, 108 deletions(-) delete mode 100644 .github/WORKFLOWS/continuous_integration.yml diff --git a/.github/WORKFLOWS/continuous_integration.yml b/.github/WORKFLOWS/continuous_integration.yml deleted file mode 100644 index 44c489d00..000000000 --- a/.github/WORKFLOWS/continuous_integration.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: tests - -on: - push: - branches: - - master - pull_request: - branches: - - master - schedule: - - cron: "0 0 1 * *" # run tests monthly - -jobs: - Linting: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.8 - uses: actions/setup-python@v1 - with: - python-version: 3.8 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox - - name: Linting code - run: tox -e lint - - name: Linting docs - run: tox -e docs - - Linux: - needs: Linting - runs-on: ubuntu-latest - strategy: - matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8] - - steps: - - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox tox-gh-actions - - name: Test with tox - run: tox - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: .coverage - - MacOS: - needs: Linting - runs-on: macos-latest - strategy: - matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8] - - steps: - - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox tox-gh-actions - - name: Test with tox - run: tox - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: .coverage - - Windows: - needs: Linting - runs-on: windows-latest - strategy: - matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8] - - steps: - - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox tox-gh-actions - - name: Test with tox - run: tox - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: .coverage diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 44c489d00..57b620653 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -30,7 +30,6 @@ jobs: run: tox -e docs Linux: - needs: Linting runs-on: ubuntu-latest strategy: matrix: @@ -55,7 +54,6 @@ jobs: file: .coverage MacOS: - needs: Linting runs-on: macos-latest strategy: matrix: @@ -80,7 +78,6 @@ jobs: file: .coverage Windows: - needs: Linting runs-on: windows-latest strategy: matrix: From 945e712d5afcfe8dce5d5c5f9bd72f05749bec27 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 5 Mar 2020 14:21:04 -0500 Subject: [PATCH 388/649] Changed CI badge from Travis to GH actions --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 2d86b39f2..d92180ea8 100644 --- a/README.rst +++ b/README.rst @@ -3,9 +3,9 @@ Arrow: Better dates & times for Python .. start-inclusion-marker-do-not-remove -.. image:: https://travis-ci.org/crsmithdev/arrow.svg?branch=master +.. image:: https://github.com/crsmithdev/arrow/workflows/tests/badge.svg?branch=master :alt: Build Status - :target: https://travis-ci.org/crsmithdev/arrow + :target: https://github.com/crsmithdev/arrow/actions?query=workflow%3Atests+branch%3Amaster .. image:: https://codecov.io/github/crsmithdev/arrow/coverage.svg?branch=master :alt: Codecov From 7e0d4f6c5eb3d8713456e3dfaa58fcc99b46ea3a Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 5 Mar 2020 16:53:25 -0500 Subject: [PATCH 389/649] Add support for parsing and formatting ISO 8601 week dates (#754) * Add support for parsing and formatting ISO 8601 week dates * Minor fixes and added failing test for discussion * Improve regex with named capture groups and simplify tests * Update docs and cleanup code --- arrow/formatter.py | 10 +++++--- arrow/parser.py | 28 +++++++++++++++++++++- docs/index.rst | 4 +++- tests/test_formatter.py | 10 +++++++- tests/test_parser.py | 51 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 6 deletions(-) diff --git a/arrow/formatter.py b/arrow/formatter.py index 80dd2c9bb..d29d5e880 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -11,12 +11,12 @@ class DateTimeFormatter(object): - # This pattern matches characters enclosed in square brackes are matched as + # 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 - # TODO: test against full timezone DB + _FORMAT_RE = re.compile( - r"(\[(?:(?=(?P[^]]))(?P=literal))*\]|YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X)" + r"(\[(?:(?=(?P[^]]))(?P=literal))*\]|YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X|W)" ) def __init__(self, locale="en_us"): @@ -116,3 +116,7 @@ def _format_token(self, dt, token): if token in ("a", "A"): return self.locale.meridian(dt.hour, token) + + if token == "W": + year, week, day = dt.isocalendar() + return "{}-W{:02d}-{}".format(year, week, day) diff --git a/arrow/parser.py b/arrow/parser.py index 379c68737..0f932b572 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -8,6 +8,7 @@ from arrow import locales from arrow.constants import MAX_TIMESTAMP, MAX_TIMESTAMP_MS, MAX_TIMESTAMP_US +from arrow.util import iso_to_gregorian try: from functools import lru_cache @@ -31,7 +32,7 @@ class ParserMatchError(ParserError): class DateTimeParser(object): _FORMAT_RE = 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)" + 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)" ) _ESCAPE_RE = re.compile(r"\[[^\[\]]*\]") @@ -49,6 +50,7 @@ class DateTimeParser(object): _TIMESTAMP_RE = re.compile(r"^\-?\d+\.?\d+$") _TIMESTAMP_EXPANDED_RE = re.compile(r"^\-?\d+$") _TIME_RE = re.compile(r"^(\d{2})(?:\:?(\d{2}))?(?:\:?(\d{2}))?(?:([\.\,])(\d+))?$") + _WEEK_DATE_RE = re.compile(r"(?P\d{4})[\-]?W(?P\d{2})[\-]?(?P\d)?") _BASE_INPUT_RE_MAP = { "YYYY": _FOUR_DIGIT_RE, @@ -73,6 +75,7 @@ class DateTimeParser(object): "ZZ": _TZ_ZZ_RE, "Z": _TZ_Z_RE, "S": _ONE_OR_MORE_DIGIT_RE, + "W": _WEEK_DATE_RE, } SEPARATORS = ["-", "/", "."] @@ -147,6 +150,7 @@ def parse_iso(self, datetime_string): "YYYY/MM", "YYYY.MM", "YYYY", + "W", ] if has_time: @@ -228,6 +232,8 @@ def parse(self, datetime_string, fmt): for token in fmt_tokens: if token == "Do": value = match.group("value") + elif token == "W": + value = (match.group("year"), match.group("week"), match.group("day")) else: value = match.group(token) self._parse_token(token, value, parts) @@ -377,9 +383,29 @@ def _parse_token(self, token, value, parts): elif value in (self.locale.meridians["pm"], self.locale.meridians["PM"]): parts["am_pm"] = "pm" + elif token == "W": + parts["weekdate"] = value + @staticmethod def _build_datetime(parts): + weekdate = parts.get("weekdate") + + if weekdate is not None: + # we can use strptime (%G, %V, %u) in python 3.6 but these tokens aren't available before that + year, week = int(weekdate[0]), int(weekdate[1]) + + if weekdate[2] is not None: + day = int(weekdate[2]) + else: + # day not given, default to 1 + day = 1 + + dt = iso_to_gregorian(year, week, day) + parts["year"] = dt.year + parts["month"] = dt.month + parts["day"] = dt.day + timestamp = parts.get("timestamp") if timestamp is not None: diff --git a/docs/index.rst b/docs/index.rst index c310f0572..f065300bc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -332,7 +332,7 @@ Then get and use a factory for it: Supported Tokens ~~~~~~~~~~~~~~~~ -Use the following tokens in parsing and formatting. Note that they're not the same as the tokens for `strptime(3) `_: +Use the following tokens for parsing and formatting. Note that they are **not** the same as the tokens for `strptime `_: +--------------------------------+--------------+-------------------------------------------+ | |Token |Output | @@ -365,6 +365,8 @@ Use the following tokens in parsing and formatting. Note that they're not the s +--------------------------------+--------------+-------------------------------------------+ | |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 | diff --git a/tests/test_formatter.py b/tests/test_formatter.py index dbf287364..5ae484d6f 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -141,6 +141,14 @@ def test_am_pm(self): assert self.formatter._format_token(dt, "a") == "pm" assert self.formatter._format_token(dt, "A") == "PM" + def test_week(self): + dt = datetime(2017, 5, 19) + assert self.formatter._format_token(dt, "W") == "2017-W20-5" + + # make sure week is zero padded when needed + dt_early = datetime(2011, 1, 20) + assert self.formatter._format_token(dt_early, "W") == "2011-W03-4" + def test_nonsense(self): dt = datetime(2012, 1, 1, 11) assert self.formatter._format_token(dt, None) is None @@ -193,5 +201,5 @@ def test_escape(self): == "Dec 31, 2017 |^${}().*+?<>-& 2:00 AM" ) - # Escaping is atomic: brackets inside brackets are treated litterally + # Escaping is atomic: brackets inside brackets are treated literally assert self.formatter.format(datetime(1, 1, 1), "[[[ ]]") == "[[ ]" diff --git a/tests/test_parser.py b/tests/test_parser.py index 039eb4c0a..3c7ee2898 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -621,6 +621,43 @@ def test_parse_HH_24(self): with pytest.raises(ParserError): 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) + assert self.parser.parse("2011W05", "W") == datetime(2011, 1, 31) + assert self.parser.parse("2011-W05-4T14:17:01", "WTHH:mm:ss") == datetime( + 2011, 2, 3, 14, 17, 1 + ) + assert self.parser.parse("2011W054T14:17:01", "WTHH:mm:ss") == datetime( + 2011, 2, 3, 14, 17, 1 + ) + assert self.parser.parse("2011-W05T14:17:01", "WTHH:mm:ss") == datetime( + 2011, 1, 31, 14, 17, 1 + ) + assert self.parser.parse("2011W05T141701", "WTHHmmss") == datetime( + 2011, 1, 31, 14, 17, 1 + ) + assert self.parser.parse("2011W054T141701", "WTHHmmss") == datetime( + 2011, 2, 3, 14, 17, 1 + ) + + bad_formats = [ + "201W22", + "1995-W1-4", + "2001-W34-90", + "2001--W34", + "2011-W03--3", + "thstrdjtrsrd676776r65", + "2002-W66-1T14:17:01", + "2002-W23-03T14:17:01", + ] + + for fmt in bad_formats: + with pytest.raises(ParserError): + self.parser.parse(fmt, "W") + class TestDateTimeParserRegex: @classmethod @@ -964,6 +1001,20 @@ def test_YYYY_MM_DDTHH_mm_ss_SZ(self): 2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzutc() ) + 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( + 2011, 2, 3, 14, 17, 1 + ) + + assert self.parser.parse_iso("2011W054") == datetime(2011, 2, 3) + + assert self.parser.parse_iso("2011W054T141701") == datetime( + 2011, 2, 3, 14, 17, 1 + ) + def test_invalid_Z(self): with pytest.raises(ParserError): From d0c993cc440d9f0b0cabe897b2e25363fa75ac01 Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Thu, 5 Mar 2020 21:58:47 +0000 Subject: [PATCH 390/649] Change GH watch button to star button (#763) --- docs/conf.py | 6 +++++- docs/index.rst | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9ae9b07b1..97cc30cfd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,7 @@ # -- Project information ----------------------------------------------------- project = u"Arrow 🏹" -copyright = "2019, Chris Smith" +copyright = "2020, Chris Smith" author = "Chris Smith" release = about["__version__"] @@ -44,6 +44,7 @@ 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.", "github_user": "crsmithdev", @@ -51,6 +52,9 @@ "github_banner": True, "show_related": False, "show_powered_by": False, + "github_button": True, + "github_type": "star", + "github_count": "true", # must be a string } html_sidebars = { diff --git a/docs/index.rst b/docs/index.rst index f065300bc..95f659b6d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ Arrow: Better dates & times for Python ====================================== -Release v\ |release|. (`Installation`_) (`Changelog `_) +Release v\ |release| (`Installation`_) (`Changelog `_) .. include:: ../README.rst :start-after: start-inclusion-marker-do-not-remove From a794566c21f1649c9feefd19b010be599294f5a9 Mon Sep 17 00:00:00 2001 From: Skactor Date: Thu, 26 Mar 2020 17:16:49 +0800 Subject: [PATCH 391/649] Update locales.py Use more precise time descriptions --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index d850f3e0a..55f19d12a 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -837,7 +837,7 @@ class ChineseCNLocale(Locale): timeframes = { "now": "刚才", "second": "一秒", - "seconds": "{0}几秒", + "seconds": "{0}秒", "minute": "1分钟", "minutes": "{0}分钟", "hour": "1小时", From 7352ae429a9f8783dfdaad2f34e8a0c25091bcb0 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Fri, 27 Mar 2020 23:50:04 +0000 Subject: [PATCH 392/649] Moving test_formatter.py to pytest. --- tests/test_formatter.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_formatter.py b/tests/test_formatter.py index 5ae484d6f..3f19a6e3b 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -9,12 +9,16 @@ from .utils import make_full_tz_list +import pytest -class TestDateTimeFormatterFormatToken: - @classmethod - def setup_class(cls): - cls.formatter = formatter.DateTimeFormatter() +@pytest.fixture(scope="class") +def formatting_fixture(request): + request.cls.formatter = formatter.DateTimeFormatter() + + +@pytest.mark.usefixtures("formatting_fixture") +class TestDateTimeFormatterFormatToken: def test_format(self): dt = datetime(2013, 2, 5, 12, 32, 51) From a0dc8bda574b3e76bf55435492b517416b40aa49 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Sat, 28 Mar 2020 01:15:45 +0000 Subject: [PATCH 393/649] Moving test_factory.py to pytest fixtures. --- tests/test_factory.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/tests/test_factory.py b/tests/test_factory.py index a94a5ade9..d968ef810 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -3,7 +3,6 @@ from datetime import date, datetime import dateparser -import pytest from dateutil import tz from arrow import factory @@ -11,12 +10,15 @@ from .utils import assert_datetime_equality +import pytest + +@pytest.fixture(scope="class") +def factory_fixture(request): + request.cls.factory = factory.ArrowFactory() -class TestGet: - @classmethod - def setup_class(cls): - cls.factory = factory.ArrowFactory() +@pytest.mark.usefixtures("factory_fixture") +class TestGet: def test_no_args(self): assert_datetime_equality( @@ -333,11 +335,8 @@ def test_locale_with_tzinfo(self): assert res.tzinfo == tz.gettz("Asia/Tokyo") +@pytest.mark.usefixtures("factory_fixture") class TestUtcNow: - @classmethod - def setup_class(cls): - cls.factory = factory.ArrowFactory() - def test_utcnow(self): assert_datetime_equality( @@ -346,11 +345,8 @@ def test_utcnow(self): ) +@pytest.mark.usefixtures("factory_fixture") class TestNow: - @classmethod - def setup_class(cls): - cls.factory = factory.ArrowFactory() - def test_no_tz(self): assert_datetime_equality(self.factory.now(), datetime.now(tz.tzlocal())) From c8f58217a5d42187342af80e9c1488857b16f7a1 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Sat, 28 Mar 2020 04:07:11 +0000 Subject: [PATCH 394/649] Moving test_parser.py to pytest fixtures. --- tests/test_parser.py | 63 ++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index 3c7ee2898..52f1868a5 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -6,7 +6,6 @@ import time from datetime import datetime -import pytest from dateutil import tz import arrow @@ -16,12 +15,26 @@ from .utils import make_full_tz_list +import pytest + + +@pytest.fixture(scope="class") +def parser_fixture(request): + request.cls.parser = parser.DateTimeParser() + + +@pytest.fixture(scope="class") +def regex_fixture(request): + request.cls.format_regex = parser.DateTimeParser._FORMAT_RE + + +@pytest.fixture(scope="class") +def tzinfo_fixture(request): + request.cls.parser = parser.TzinfoParser() -class TestDateTimeParser: - @classmethod - def setup_class(cls): - cls.parser = parser.DateTimeParser() +@pytest.mark.usefixtures("parser_fixture") +class TestDateTimeParser: def test_parse_multiformat(self, mocker): mocker.patch( "arrow.parser.DateTimeParser.parse", @@ -168,11 +181,8 @@ def test_timestamp_format_list(self): self.parser.parse("15 Jul", "X") +@pytest.mark.usefixtures("parser_fixture") class TestDateTimeParserParse: - @classmethod - def setup_class(cls): - cls.parser = parser.DateTimeParser() - def test_parse_list(self, mocker): mocker.patch( @@ -659,11 +669,8 @@ def test_parse_W(self): self.parser.parse(fmt, "W") +@pytest.mark.usefixtures("regex_fixture") class TestDateTimeParserRegex: - @classmethod - def setup_class(cls): - cls.format_regex = parser.DateTimeParser._FORMAT_RE - def test_format_year(self): assert self.format_regex.findall("YYYY-YY") == ["YYYY", "YY"] @@ -810,11 +817,8 @@ def test_time(self): assert time_re.findall("12:35:46,") == [] +@pytest.mark.usefixtures("parser_fixture") class TestDateTimeParserISO: - @classmethod - def setup_class(cls): - cls.parser = parser.DateTimeParser("en_us") - def test_YYYY(self): assert self.parser.parse_iso("2013") == datetime(2013, 1, 1) @@ -1217,11 +1221,8 @@ def test_midnight_end_day(self): self.parser.parse_iso("2019-12-31T24:00:00.999999") +@pytest.mark.usefixtures("tzinfo_fixture") class TestTzinfoParser: - @classmethod - def setup_class(cls): - cls.parser = parser.TzinfoParser() - def test_parse_local(self): assert self.parser.parse("local") == tz.tzlocal() @@ -1256,11 +1257,8 @@ def test_parse_fails(self): self.parser.parse("fail") +@pytest.mark.usefixtures("parser_fixture") class TestDateTimeParserMonthName: - @classmethod - def setup_class(cls): - cls.parser = parser.DateTimeParser("en_us") - def test_shortmonth_capitalized(self): assert self.parser.parse("2013-Jan-01", "YYYY-MMM-DD") == datetime(2013, 1, 1) @@ -1302,11 +1300,8 @@ def test_localized_month_abbreviation(self): assert parser_.parse("2013-Gen-01", "YYYY-MMM-DD") == datetime(2013, 1, 1) +@pytest.mark.usefixtures("parser_fixture") class TestDateTimeParserMeridians: - @classmethod - def setup_class(cls): - cls.parser = parser.DateTimeParser("en_us") - def test_meridians_lowercase(self): assert self.parser.parse("2013-01-01 5am", "YYYY-MM-DD ha") == datetime( 2013, 1, 1, 5 @@ -1366,11 +1361,8 @@ def test_fr_meridians(self): parser_.parse("Janvier 30, 2019 - 08:00 pm", "MMMM DD, YYYY - hh:mm a") +@pytest.mark.usefixtures("parser_fixture") class TestDateTimeParserMonthOrdinalDay: - @classmethod - def setup_class(cls): - cls.parser = parser.DateTimeParser("en_us") - def test_english(self): parser_ = parser.DateTimeParser("en_us") @@ -1436,11 +1428,8 @@ def test_french(self): ) +@pytest.mark.usefixtures("parser_fixture") class TestDateTimeParserSearchDate: - @classmethod - def setup_class(cls): - cls.parser = parser.DateTimeParser() - def test_parse_search(self): assert self.parser.parse( From d4da708c76c744f8f3c0af66eb77c20d7cf1bb1c Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Sat, 28 Mar 2020 19:02:04 +0000 Subject: [PATCH 395/649] Moving test_locales.py to pytest fixtures. --- arrow/locales.py | 14 +++ tests/test_locales.py | 270 ++++++++++++++++-------------------------- 2 files changed, 115 insertions(+), 169 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 55f19d12a..c56b38934 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -21,6 +21,20 @@ def get_locale(name): return locale_cls() +def get_locale_by_class_name(name): + """Returns an appropriate :class:`Locale ` + corresponding to an locale class name. + + :param name: the name of the locale class. + + """ + locale_cls = globals()[name] + + if locale_cls is None: + raise ValueError("Unsupported locale '{}'".format(name)) + + return locale_cls() + # base locale type. diff --git a/tests/test_locales.py b/tests/test_locales.py index 6002968f6..3d2cfd526 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1,18 +1,27 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from arrow import arrow, locales + import pytest -from arrow import arrow, locales +@pytest.fixture(scope="class") +def locales_fixture(request): + request.cls.locales = locales._locales -class TestLocaleValidation: - """Validate locales to ensure that translations are valid and complete""" +@pytest.fixture(scope="class") +def lang_locale_fixture(request): + name = request.cls.__name__[4:] + if name == 'Locale': + request.cls.locale = locales.get_locale_by_class_name('EnglishLocale') + else: + request.cls.locale = locales.get_locale_by_class_name(name) - @classmethod - def setup_class(cls): - cls.locales = locales._locales +@pytest.mark.usefixtures("locales_fixture") +class TestLocaleValidation: + """Validate locales to ensure that translations are valid and complete""" def test_locale_validation(self): for _, locale_cls in self.locales.items(): @@ -61,10 +70,8 @@ def test_locales(self): assert len(locales._locales) > 0 +@pytest.mark.usefixtures("lang_locale_fixture") class TestLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.EnglishLocale() def test_format_timeframe(self): @@ -126,87 +133,79 @@ def test_meridian_invalid_token(self): assert self.locale.meridian(7, "NONSENSE") is None +@pytest.mark.usefixtures("lang_locale_fixture") class TestEnglishLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.EnglishLocale() - def test_describe(self): assert self.locale.describe("now", only_distance=True) == "instantly" assert self.locale.describe("now", only_distance=False) == "just now" -class TestItalianLocales: +@pytest.mark.usefixtures("lang_locale_fixture") +class TestItalianLocale: def test_ordinal_number(self): - locale = locales.ItalianLocale() + assert self.locale.ordinal_number(1) == "1º" - assert locale.ordinal_number(1) == "1º" - -class TestSpanishLocales: +@pytest.mark.usefixtures("lang_locale_fixture") +class TestSpanishLocale: def test_ordinal_number(self): - locale = locales.SpanishLocale() - - assert locale.ordinal_number(1) == "1º" + assert self.locale.ordinal_number(1) == "1º" def test_format_timeframe(self): - locale = locales.SpanishLocale() - assert locale._format_timeframe("now", 0) == "ahora" - assert locale._format_timeframe("seconds", 1) == "1 segundos" - assert locale._format_timeframe("seconds", 3) == "3 segundos" - assert locale._format_timeframe("seconds", 30) == "30 segundos" - assert locale._format_timeframe("minute", 1) == "un minuto" - assert locale._format_timeframe("minutes", 4) == "4 minutos" - assert locale._format_timeframe("minutes", 40) == "40 minutos" - assert locale._format_timeframe("hour", 1) == "una hora" - assert locale._format_timeframe("hours", 5) == "5 horas" - assert locale._format_timeframe("hours", 23) == "23 horas" - assert locale._format_timeframe("day", 1) == "un día" - assert locale._format_timeframe("days", 6) == "6 días" - assert locale._format_timeframe("days", 12) == "12 días" - assert locale._format_timeframe("week", 1) == "una semana" - assert locale._format_timeframe("weeks", 2) == "2 semanas" - assert locale._format_timeframe("weeks", 3) == "3 semanas" - assert locale._format_timeframe("month", 1) == "un mes" - assert locale._format_timeframe("months", 7) == "7 meses" - assert locale._format_timeframe("months", 11) == "11 meses" - assert locale._format_timeframe("year", 1) == "un año" - assert locale._format_timeframe("years", 8) == "8 años" - assert locale._format_timeframe("years", 12) == "12 años" - - assert locale._format_timeframe("now", 0) == "ahora" - assert locale._format_timeframe("seconds", -1) == "1 segundos" - assert locale._format_timeframe("seconds", -9) == "9 segundos" - assert locale._format_timeframe("seconds", -12) == "12 segundos" - assert locale._format_timeframe("minute", -1) == "un minuto" - assert locale._format_timeframe("minutes", -2) == "2 minutos" - assert locale._format_timeframe("minutes", -10) == "10 minutos" - assert locale._format_timeframe("hour", -1) == "una hora" - assert locale._format_timeframe("hours", -3) == "3 horas" - assert locale._format_timeframe("hours", -11) == "11 horas" - assert locale._format_timeframe("day", -1) == "un día" - assert locale._format_timeframe("days", -2) == "2 días" - assert locale._format_timeframe("days", -12) == "12 días" - assert locale._format_timeframe("week", -1) == "una semana" - assert locale._format_timeframe("weeks", -2) == "2 semanas" - assert locale._format_timeframe("weeks", -3) == "3 semanas" - assert locale._format_timeframe("month", -1) == "un mes" - assert locale._format_timeframe("months", -3) == "3 meses" - assert locale._format_timeframe("months", -13) == "13 meses" - assert locale._format_timeframe("year", -1) == "un año" - assert locale._format_timeframe("years", -4) == "4 años" - assert locale._format_timeframe("years", -14) == "14 años" - - -class TestFrenchLocales: + assert self.locale._format_timeframe("now", 0) == "ahora" + assert self.locale._format_timeframe("seconds", 1) == "1 segundos" + assert self.locale._format_timeframe("seconds", 3) == "3 segundos" + assert self.locale._format_timeframe("seconds", 30) == "30 segundos" + assert self.locale._format_timeframe("minute", 1) == "un minuto" + assert self.locale._format_timeframe("minutes", 4) == "4 minutos" + assert self.locale._format_timeframe("minutes", 40) == "40 minutos" + assert self.locale._format_timeframe("hour", 1) == "una hora" + assert self.locale._format_timeframe("hours", 5) == "5 horas" + assert self.locale._format_timeframe("hours", 23) == "23 horas" + assert self.locale._format_timeframe("day", 1) == "un día" + assert self.locale._format_timeframe("days", 6) == "6 días" + assert self.locale._format_timeframe("days", 12) == "12 días" + assert self.locale._format_timeframe("week", 1) == "una semana" + assert self.locale._format_timeframe("weeks", 2) == "2 semanas" + assert self.locale._format_timeframe("weeks", 3) == "3 semanas" + assert self.locale._format_timeframe("month", 1) == "un mes" + assert self.locale._format_timeframe("months", 7) == "7 meses" + assert self.locale._format_timeframe("months", 11) == "11 meses" + assert self.locale._format_timeframe("year", 1) == "un año" + assert self.locale._format_timeframe("years", 8) == "8 años" + assert self.locale._format_timeframe("years", 12) == "12 años" + + assert self.locale._format_timeframe("now", 0) == "ahora" + assert self.locale._format_timeframe("seconds", -1) == "1 segundos" + assert self.locale._format_timeframe("seconds", -9) == "9 segundos" + assert self.locale._format_timeframe("seconds", -12) == "12 segundos" + assert self.locale._format_timeframe("minute", -1) == "un minuto" + assert self.locale._format_timeframe("minutes", -2) == "2 minutos" + assert self.locale._format_timeframe("minutes", -10) == "10 minutos" + assert self.locale._format_timeframe("hour", -1) == "una hora" + assert self.locale._format_timeframe("hours", -3) == "3 horas" + assert self.locale._format_timeframe("hours", -11) == "11 horas" + assert self.locale._format_timeframe("day", -1) == "un día" + assert self.locale._format_timeframe("days", -2) == "2 días" + assert self.locale._format_timeframe("days", -12) == "12 días" + assert self.locale._format_timeframe("week", -1) == "una semana" + assert self.locale._format_timeframe("weeks", -2) == "2 semanas" + assert self.locale._format_timeframe("weeks", -3) == "3 semanas" + assert self.locale._format_timeframe("month", -1) == "un mes" + assert self.locale._format_timeframe("months", -3) == "3 meses" + assert self.locale._format_timeframe("months", -13) == "13 meses" + assert self.locale._format_timeframe("year", -1) == "un año" + assert self.locale._format_timeframe("years", -4) == "4 años" + assert self.locale._format_timeframe("years", -14) == "14 años" + + +@pytest.mark.usefixtures("lang_locale_fixture") +class TestFrenchLocale: def test_ordinal_number(self): - locale = locales.FrenchLocale() - - assert locale.ordinal_number(1) == "1er" - assert locale.ordinal_number(2) == "2e" + assert self.locale.ordinal_number(1) == "1er" + assert self.locale.ordinal_number(2) == "2e" - -class TestRussianLocales: +class TestRussianLocale: def test_plurals2(self): locale = locales.RussianLocale() @@ -231,7 +230,7 @@ def test_plurals2(self): assert locale._format_timeframe("minutes", 25) == "25 минут" -class TestPolishLocales: +class TestPolishLocale: def test_plurals(self): locale = locales.PolishLocale() @@ -246,11 +245,8 @@ def test_plurals(self): assert locale._format_timeframe("hours", 25) == "25 godzin" -class TestIcelandicLocales: - @classmethod - def setup_class(cls): - cls.locale = locales.IcelandicLocale() - +@pytest.mark.usefixtures("lang_locale_fixture") +class TestIcelandicLocale: def test_format_timeframe(self): assert self.locale._format_timeframe("minute", -1) == "einni mínútu" @@ -260,12 +256,9 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 2) == "2 tíma" assert self.locale._format_timeframe("now", 0) == "rétt í þessu" +@pytest.mark.usefixtures("lang_locale_fixture") class TestMalayalamLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.MalayalamLocale() - def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 2) == "2 മണിക്കൂർ" @@ -287,12 +280,9 @@ def test_format_relative_future(self): result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", -1) assert result == "ഒരു മണിക്കൂർ മുമ്പ്" +@pytest.mark.usefixtures("lang_locale_fixture") class TestHindiLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.HindiLocale() - def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 2) == "2 घंटे" @@ -314,12 +304,8 @@ def test_format_relative_future(self): result = self.locale._format_relative("एक घंटा", "hour", -1) assert result == "एक घंटा पहले" - +@pytest.mark.usefixtures("lang_locale_fixture") class TestCzechLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.CzechLocale() - def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 2) == "2 hodiny" @@ -344,11 +330,9 @@ def test_format_relative_past(self): result = self.locale._format_relative("hodinou", "hour", -1) assert result == "Před hodinou" +@pytest.mark.usefixtures("lang_locale_fixture") class TestSlovakLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.SlovakLocale() def test_format_timeframe(self): @@ -443,12 +427,8 @@ def test_couple_of_timeframe(self): assert locale._format_timeframe("days", 3) == "3 ימים" assert locale._format_timeframe("years", 5) == "5 שנים" - +@pytest.mark.usefixtures("lang_locale_fixture") class TestMarathiLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.MarathiLocale() - def test_dateCoreFunctionality(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) assert self.locale.month_name(dt.month) == "एप्रिल" @@ -477,11 +457,8 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1" +@pytest.mark.usefixtures("lang_locale_fixture") class TestFinnishLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.FinnishLocale() - def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 2) == ("2 tuntia", "2 tunnin") assert self.locale._format_timeframe("hour", 0) == ("tunti", "tunnin") @@ -502,11 +479,8 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1." +@pytest.mark.usefixtures("lang_locale_fixture") class TestGermanLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.GermanLocale() - def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1." @@ -523,11 +497,8 @@ def test_define(self): assert self.locale.describe("year", only_distance=False) == "in einem Jahr" +@pytest.mark.usefixtures("lang_locale_fixture") class TestHungarianLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.HungarianLocale() - def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 2) == "2 óra" assert self.locale._format_timeframe("hour", 0) == "egy órával" @@ -535,11 +506,8 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("now", 0) == "éppen most" +@pytest.mark.usefixtures("lang_locale_fixture") class TestEsperantoLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.EsperantoLocale() - def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 2) == "2 horoj" assert self.locale._format_timeframe("hour", 0) == "un horo" @@ -550,11 +518,8 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1a" +@pytest.mark.usefixtures("lang_locale_fixture") class TestThaiLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.ThaiLocale() - def test_year_full(self): assert self.locale.year_full(2015) == "2558" @@ -578,11 +543,8 @@ def test_format_relative_future(self): assert result == "1 ชั่วโมง ที่ผ่านมา" +@pytest.mark.usefixtures("lang_locale_fixture") class TestBengaliLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.BengaliLocale() - def test_ordinal_number(self): result0 = self.locale._ordinal_number(0) result1 = self.locale._ordinal_number(1) @@ -605,11 +567,8 @@ def test_ordinal_number(self): assert self.locale._ordinal_number(-1) is None +@pytest.mark.usefixtures("lang_locale_fixture") class TestSwissLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.SwissLocale() - def test_ordinal_number(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) @@ -618,11 +577,8 @@ def test_ordinal_number(self): assert self.locale.day_abbreviation(dt.isoweekday()) == "Sa" +@pytest.mark.usefixtures("lang_locale_fixture") class TestRomanianLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.RomanianLocale() - def test_timeframes(self): assert self.locale._format_timeframe("hours", 2) == "2 ore" @@ -656,11 +612,8 @@ def test_relative_timeframes(self): assert self.locale._format_relative("o zi", "day", 1) == "peste o zi" -class TestArabicLocales: - @classmethod - def setup_class(cls): - cls.locale = locales.ArabicLocale() - +@pytest.mark.usefixtures("lang_locale_fixture") +class TestArabicLocale: def test_timeframes(self): # single @@ -692,11 +645,8 @@ def test_timeframes(self): assert self.locale._format_timeframe("years", 115) == "115 سنة" +@pytest.mark.usefixtures("lang_locale_fixture") class TestNepaliLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.NepaliLocale() - def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 3) == "3 घण्टा" assert self.locale._format_timeframe("hour", 0) == "एक घण्टा" @@ -714,11 +664,8 @@ def test_format_relative_past(self): assert result == "एक घण्टा पहिले" +@pytest.mark.usefixtures("lang_locale_fixture") class TestIndonesianLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.IndonesianLocale() - def test_timeframes(self): assert self.locale._format_timeframe("hours", 2) == "2 jam" assert self.locale._format_timeframe("months", 2) == "2 bulan" @@ -742,11 +689,8 @@ def test_format_relative_future(self): assert self.locale._format_relative("1 jam", "hour", -1) == "1 jam yang lalu" +@pytest.mark.usefixtures("lang_locale_fixture") class TestTagalogLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.TagalogLocale() - def test_format_timeframe(self): assert self.locale._format_timeframe("minute", 1) == "isang minuto" @@ -781,11 +725,8 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(114) == "ika-114" +@pytest.mark.usefixtures("lang_locale_fixture") class TestEstonianLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.EstonianLocale() - def test_format_timeframe(self): assert self.locale._format_timeframe("now", 0) == "just nüüd" assert self.locale._format_timeframe("second", 1) == "ühe sekundi" @@ -828,11 +769,8 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("years", -14) == "14 aastat" +@pytest.mark.usefixtures("lang_locale_fixture") class TestPortugueseLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.PortugueseLocale() - def test_format_timeframe(self): assert self.locale._format_timeframe("now", 0) == "agora" assert self.locale._format_timeframe("second", 1) == "um segundo" @@ -849,11 +787,8 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("years", 12) == "12 anos" -class TestBrazilianLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.BrazilianPortugueseLocale() - +@pytest.mark.usefixtures("lang_locale_fixture") +class TestBrazilianPortugueseLocale: def test_format_timeframe(self): assert self.locale._format_timeframe("now", 0) == "agora" assert self.locale._format_timeframe("second", 1) == "um segundo" @@ -870,11 +805,8 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("years", 12) == "12 anos" +@pytest.mark.usefixtures("lang_locale_fixture") class TestHongKongLocale: - @classmethod - def setup_class(cls): - cls.locale = locales.HongKongLocale() - def test_format_timeframe(self): assert self.locale._format_timeframe("now", 0) == "剛才" assert self.locale._format_timeframe("second", 1) == "1秒" From 714f4ca4924a44eaa05d03f8ee41427c790adc7a Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Sat, 28 Mar 2020 19:21:22 +0000 Subject: [PATCH 396/649] Finishing up moving test_locales.py to pytest fixtures. --- tests/test_locales.py | 188 +++++++++++++++++++----------------------- 1 file changed, 85 insertions(+), 103 deletions(-) diff --git a/tests/test_locales.py b/tests/test_locales.py index 3d2cfd526..0c4966db2 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -10,6 +10,7 @@ def locales_fixture(request): request.cls.locales = locales._locales + @pytest.fixture(scope="class") def lang_locale_fixture(request): name = request.cls.__name__[4:] @@ -72,7 +73,6 @@ def test_locales(self): @pytest.mark.usefixtures("lang_locale_fixture") class TestLocale: - def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 2) == "2 hours" @@ -205,44 +205,41 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1er" assert self.locale.ordinal_number(2) == "2e" + +@pytest.mark.usefixtures("lang_locale_fixture") class TestRussianLocale: def test_plurals2(self): - - locale = locales.RussianLocale() - - assert locale._format_timeframe("hours", 0) == "0 часов" - assert locale._format_timeframe("hours", 1) == "1 час" - assert locale._format_timeframe("hours", 2) == "2 часа" - assert locale._format_timeframe("hours", 4) == "4 часа" - assert locale._format_timeframe("hours", 5) == "5 часов" - assert locale._format_timeframe("hours", 21) == "21 час" - assert locale._format_timeframe("hours", 22) == "22 часа" - assert locale._format_timeframe("hours", 25) == "25 часов" + assert self.locale._format_timeframe("hours", 0) == "0 часов" + assert self.locale._format_timeframe("hours", 1) == "1 час" + assert self.locale._format_timeframe("hours", 2) == "2 часа" + assert self.locale._format_timeframe("hours", 4) == "4 часа" + assert self.locale._format_timeframe("hours", 5) == "5 часов" + assert self.locale._format_timeframe("hours", 21) == "21 час" + assert self.locale._format_timeframe("hours", 22) == "22 часа" + assert self.locale._format_timeframe("hours", 25) == "25 часов" # feminine grammatical gender should be tested separately - assert locale._format_timeframe("minutes", 0) == "0 минут" - assert locale._format_timeframe("minutes", 1) == "1 минуту" - assert locale._format_timeframe("minutes", 2) == "2 минуты" - assert locale._format_timeframe("minutes", 4) == "4 минуты" - assert locale._format_timeframe("minutes", 5) == "5 минут" - assert locale._format_timeframe("minutes", 21) == "21 минуту" - assert locale._format_timeframe("minutes", 22) == "22 минуты" - assert locale._format_timeframe("minutes", 25) == "25 минут" + assert self.locale._format_timeframe("minutes", 0) == "0 минут" + assert self.locale._format_timeframe("minutes", 1) == "1 минуту" + assert self.locale._format_timeframe("minutes", 2) == "2 минуты" + assert self.locale._format_timeframe("minutes", 4) == "4 минуты" + assert self.locale._format_timeframe("minutes", 5) == "5 минут" + assert self.locale._format_timeframe("minutes", 21) == "21 минуту" + assert self.locale._format_timeframe("minutes", 22) == "22 минуты" + assert self.locale._format_timeframe("minutes", 25) == "25 минут" +@pytest.mark.usefixtures("lang_locale_fixture") class TestPolishLocale: def test_plurals(self): - - locale = locales.PolishLocale() - - assert locale._format_timeframe("hours", 0) == "0 godzin" - assert locale._format_timeframe("hours", 1) == "1 godzin" - assert locale._format_timeframe("hours", 2) == "2 godziny" - assert locale._format_timeframe("hours", 4) == "4 godziny" - assert locale._format_timeframe("hours", 5) == "5 godzin" - assert locale._format_timeframe("hours", 21) == "21 godzin" - assert locale._format_timeframe("hours", 22) == "22 godziny" - assert locale._format_timeframe("hours", 25) == "25 godzin" + assert self.locale._format_timeframe("hours", 0) == "0 godzin" + assert self.locale._format_timeframe("hours", 1) == "1 godzin" + assert self.locale._format_timeframe("hours", 2) == "2 godziny" + assert self.locale._format_timeframe("hours", 4) == "4 godziny" + assert self.locale._format_timeframe("hours", 5) == "5 godzin" + assert self.locale._format_timeframe("hours", 21) == "21 godzin" + assert self.locale._format_timeframe("hours", 22) == "22 godziny" + assert self.locale._format_timeframe("hours", 25) == "25 godzin" @pytest.mark.usefixtures("lang_locale_fixture") @@ -256,8 +253,8 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 2) == "2 tíma" assert self.locale._format_timeframe("now", 0) == "rétt í þessu" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale_fixture") class TestMalayalamLocale: def test_format_timeframe(self): @@ -280,8 +277,8 @@ def test_format_relative_future(self): result = self.locale._format_relative("ഒരു മണിക്കൂർ", "hour", -1) assert result == "ഒരു മണിക്കൂർ മുമ്പ്" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale_fixture") class TestHindiLocale: def test_format_timeframe(self): @@ -291,7 +288,6 @@ def test_format_timeframe(self): def test_format_relative_now(self): result = self.locale._format_relative("अभी", "now", 0) - assert result == "अभी" def test_format_relative_past(self): @@ -304,6 +300,7 @@ def test_format_relative_future(self): result = self.locale._format_relative("एक घंटा", "hour", -1) assert result == "एक घंटा पहले" + @pytest.mark.usefixtures("lang_locale_fixture") class TestCzechLocale: def test_format_timeframe(self): @@ -330,10 +327,9 @@ def test_format_relative_past(self): result = self.locale._format_relative("hodinou", "hour", -1) assert result == "Před hodinou" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale_fixture") class TestSlovakLocale: - def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 2) == "2 hodiny" @@ -359,73 +355,68 @@ def test_format_relative_past(self): assert result == "Pred hodinou" +@pytest.mark.usefixtures("lang_locale_fixture") class TestBulgarianLocale: def test_plurals2(self): - - locale = locales.BulgarianLocale() - - assert locale._format_timeframe("hours", 0) == "0 часа" - assert locale._format_timeframe("hours", 1) == "1 час" - assert locale._format_timeframe("hours", 2) == "2 часа" - assert locale._format_timeframe("hours", 4) == "4 часа" - assert locale._format_timeframe("hours", 5) == "5 часа" - assert locale._format_timeframe("hours", 21) == "21 час" - assert locale._format_timeframe("hours", 22) == "22 часа" - assert locale._format_timeframe("hours", 25) == "25 часа" + assert self.locale._format_timeframe("hours", 0) == "0 часа" + assert self.locale._format_timeframe("hours", 1) == "1 час" + assert self.locale._format_timeframe("hours", 2) == "2 часа" + assert self.locale._format_timeframe("hours", 4) == "4 часа" + assert self.locale._format_timeframe("hours", 5) == "5 часа" + assert self.locale._format_timeframe("hours", 21) == "21 час" + assert self.locale._format_timeframe("hours", 22) == "22 часа" + assert self.locale._format_timeframe("hours", 25) == "25 часа" # feminine grammatical gender should be tested separately - assert locale._format_timeframe("minutes", 0) == "0 минути" - assert locale._format_timeframe("minutes", 1) == "1 минута" - assert locale._format_timeframe("minutes", 2) == "2 минути" - assert locale._format_timeframe("minutes", 4) == "4 минути" - assert locale._format_timeframe("minutes", 5) == "5 минути" - assert locale._format_timeframe("minutes", 21) == "21 минута" - assert locale._format_timeframe("minutes", 22) == "22 минути" - assert locale._format_timeframe("minutes", 25) == "25 минути" + assert self.locale._format_timeframe("minutes", 0) == "0 минути" + assert self.locale._format_timeframe("minutes", 1) == "1 минута" + assert self.locale._format_timeframe("minutes", 2) == "2 минути" + assert self.locale._format_timeframe("minutes", 4) == "4 минути" + assert self.locale._format_timeframe("minutes", 5) == "5 минути" + assert self.locale._format_timeframe("minutes", 21) == "21 минута" + assert self.locale._format_timeframe("minutes", 22) == "22 минути" + assert self.locale._format_timeframe("minutes", 25) == "25 минути" +@pytest.mark.usefixtures("lang_locale_fixture") class TestMacedonianLocale: def test_plurals_mk(self): - - locale = locales.MacedonianLocale() - # time - assert locale._format_relative("сега", "now", 0) == "сега" + assert self.locale._format_relative("сега", "now", 0) == "сега" # Hours - assert locale._format_timeframe("hours", 0) == "0 саати" - assert locale._format_timeframe("hours", 1) == "1 саат" - assert locale._format_timeframe("hours", 2) == "2 саати" - assert locale._format_timeframe("hours", 4) == "4 саати" - assert locale._format_timeframe("hours", 5) == "5 саати" - assert locale._format_timeframe("hours", 21) == "21 саат" - assert locale._format_timeframe("hours", 22) == "22 саати" - assert locale._format_timeframe("hours", 25) == "25 саати" + assert self.locale._format_timeframe("hours", 0) == "0 саати" + assert self.locale._format_timeframe("hours", 1) == "1 саат" + assert self.locale._format_timeframe("hours", 2) == "2 саати" + assert self.locale._format_timeframe("hours", 4) == "4 саати" + assert self.locale._format_timeframe("hours", 5) == "5 саати" + assert self.locale._format_timeframe("hours", 21) == "21 саат" + assert self.locale._format_timeframe("hours", 22) == "22 саати" + assert self.locale._format_timeframe("hours", 25) == "25 саати" # Minutes - assert locale._format_timeframe("minutes", 0) == "0 минути" - assert locale._format_timeframe("minutes", 1) == "1 минута" - assert locale._format_timeframe("minutes", 2) == "2 минути" - assert locale._format_timeframe("minutes", 4) == "4 минути" - assert locale._format_timeframe("minutes", 5) == "5 минути" - assert locale._format_timeframe("minutes", 21) == "21 минута" - assert locale._format_timeframe("minutes", 22) == "22 минути" - assert locale._format_timeframe("minutes", 25) == "25 минути" + assert self.locale._format_timeframe("minutes", 0) == "0 минути" + assert self.locale._format_timeframe("minutes", 1) == "1 минута" + assert self.locale._format_timeframe("minutes", 2) == "2 минути" + assert self.locale._format_timeframe("minutes", 4) == "4 минути" + assert self.locale._format_timeframe("minutes", 5) == "5 минути" + assert self.locale._format_timeframe("minutes", 21) == "21 минута" + assert self.locale._format_timeframe("minutes", 22) == "22 минути" + assert self.locale._format_timeframe("minutes", 25) == "25 минути" +@pytest.mark.usefixtures("lang_locale_fixture") class TestHebrewLocale: def test_couple_of_timeframe(self): - locale = locales.HebrewLocale() - - assert locale._format_timeframe("hours", 2) == "שעתיים" - assert locale._format_timeframe("months", 2) == "חודשיים" - assert locale._format_timeframe("days", 2) == "יומיים" - assert locale._format_timeframe("years", 2) == "שנתיים" + assert self.locale._format_timeframe("hours", 2) == "שעתיים" + assert self.locale._format_timeframe("months", 2) == "חודשיים" + assert self.locale._format_timeframe("days", 2) == "יומיים" + assert self.locale._format_timeframe("years", 2) == "שנתיים" + assert self.locale._format_timeframe("hours", 3) == "3 שעות" + assert self.locale._format_timeframe("months", 4) == "4 חודשים" + assert self.locale._format_timeframe("days", 3) == "3 ימים" + assert self.locale._format_timeframe("years", 5) == "5 שנים" - assert locale._format_timeframe("hours", 3) == "3 שעות" - assert locale._format_timeframe("months", 4) == "4 חודשים" - assert locale._format_timeframe("days", 3) == "3 ימים" - assert locale._format_timeframe("years", 5) == "5 שנים" @pytest.mark.usefixtures("lang_locale_fixture") class TestMarathiLocale: @@ -546,24 +537,15 @@ def test_format_relative_future(self): @pytest.mark.usefixtures("lang_locale_fixture") class TestBengaliLocale: def test_ordinal_number(self): - result0 = self.locale._ordinal_number(0) - result1 = self.locale._ordinal_number(1) - result3 = self.locale._ordinal_number(3) - result4 = self.locale._ordinal_number(4) - result5 = self.locale._ordinal_number(5) - result6 = self.locale._ordinal_number(6) - result10 = self.locale._ordinal_number(10) - result11 = self.locale._ordinal_number(11) - result42 = self.locale._ordinal_number(42) - assert result0 == "0তম" - assert result1 == "1ম" - assert result3 == "3য়" - assert result4 == "4র্থ" - assert result5 == "5ম" - assert result6 == "6ষ্ঠ" - assert result10 == "10ম" - assert result11 == "11তম" - assert result42 == "42তম" + assert self.locale._ordinal_number(0) == "0তম" + assert self.locale._ordinal_number(1) == "1ম" + assert self.locale._ordinal_number(3) == "3য়" + assert self.locale._ordinal_number(4) == "4র্থ" + assert self.locale._ordinal_number(5) == "5ম" + assert self.locale._ordinal_number(6) == "6ষ্ঠ" + 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 From c73e57f63c89fe523b5cc00aa201da3252f70819 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Sat, 28 Mar 2020 19:32:53 +0000 Subject: [PATCH 397/649] Moving test_arrow.py to use pytest fixtures. --- tests/test_arrow.py | 47 ++++++++++++++++----------------------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 58c035e00..c4dc7308f 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -18,6 +18,17 @@ from .utils import assert_datetime_equality +@pytest.fixture(scope="class") +def utcnow_fixture(request): + request.cls.arrow = arrow.Arrow.utcnow() + + +@pytest.fixture(scope="class") +def time2013_01_01_fixture(request): + request.cls.now = arrow.Arrow.utcnow() + request.cls.arrow = arrow.Arrow(2013, 1, 1) + request.cls.datetime = datetime(2013, 1, 1) + class TestTestArrowInit: def test_init_bad_input(self): @@ -221,12 +232,8 @@ def test_clone(self): assert result is not self.arrow assert result._datetime == self.arrow._datetime - +@pytest.mark.usefixtures("time2013_01_01_fixture") class TestArrowAttribute: - @classmethod - def setup_class(cls): - cls.arrow = arrow.Arrow(2013, 1, 1) - def test_getattr_base(self): with pytest.raises(AttributeError): @@ -283,11 +290,8 @@ def test_float_timestamp(self): assert result == self.arrow.microsecond +@pytest.mark.usefixtures("utcnow_fixture") class TestArrowComparison: - @classmethod - def setup_class(cls): - cls.arrow = arrow.Arrow.utcnow() - def test_eq(self): assert self.arrow == self.arrow @@ -342,12 +346,8 @@ def test_le(self): assert self.arrow <= self.arrow assert self.arrow <= self.arrow.datetime - +@pytest.mark.usefixtures('time2013_01_01_fixture') class TestArrowMath: - @classmethod - def setup_class(cls): - cls.arrow = arrow.Arrow(2013, 1, 1) - def test_add_timedelta(self): result = self.arrow.__add__(timedelta(days=1)) @@ -399,12 +399,8 @@ def test_rsub_other(self): with pytest.raises(TypeError): timedelta(days=1) - self.arrow - +@pytest.mark.usefixtures("utcnow_fixture") class TestArrowDatetimeInterface: - @classmethod - def setup_class(cls): - cls.arrow = arrow.Arrow.utcnow() - def test_date(self): result = self.arrow.date() @@ -1319,13 +1315,8 @@ def test_bounds_are_validated(self): with pytest.raises(AttributeError): floor, ceil = self.arrow.span("hour", bounds="][") - +@pytest.mark.usefixtures("time2013_01_01_fixture") class TestArrowHumanize: - @classmethod - def setup_class(cls): - cls.datetime = datetime(2013, 1, 1) - cls.now = arrow.Arrow.utcnow() - def test_granularity(self): assert self.now.humanize(granularity="second") == "just now" @@ -1693,12 +1684,8 @@ def test_untranslated_granularity(self, mocker): with pytest.raises(ValueError): arw.humanize(later, granularity="week") - +@pytest.mark.usefixtures("time2013_01_01_fixture") class TestArrowHumanizeTestsWithLocale: - @classmethod - def setup_class(cls): - cls.datetime = datetime(2013, 1, 1) - def test_now(self): arw = arrow.Arrow(2013, 1, 1, 0, 0, 0) From 4f8e1328d1f2d45b9ebba128a3a16d974d1a5a8c Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Sun, 29 Mar 2020 00:38:42 +0000 Subject: [PATCH 398/649] Moving test_arrow.py to use pytest fixtures. --- tests/test_arrow.py | 69 ++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/tests/test_arrow.py b/tests/test_arrow.py index c4dc7308f..9b273e68c 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -29,6 +29,38 @@ def time2013_01_01_fixture(request): request.cls.arrow = arrow.Arrow(2013, 1, 1) request.cls.datetime = datetime(2013, 1, 1) + +@pytest.fixture(scope="class") +def time2013_02_03_fixture(request): + request.cls.arrow = arrow.Arrow(2013, 2, 3, 12, 30, 45, 1) + + +@pytest.fixture(scope="class") +def time2013_02_15_fixture(request): + request.cls.datetime = datetime(2013, 2, 15, 3, 41, 22, 8923) + request.cls.arrow = arrow.Arrow.fromdatetime(request.cls.datetime) + + +@pytest.fixture(scope="class") +def timedst_fixture(request): + request.cls.before_1 = arrow.Arrow( + 2016, 11, 6, 3, 59, tzinfo=tz.gettz("America/New_York") + ) + request.cls.before_2 = arrow.Arrow(2016, 11, 6, tzinfo=tz.gettz("America/New_York")) + request.cls.after_1 = arrow.Arrow(2016, 11, 6, 4, tzinfo=tz.gettz("America/New_York")) + request.cls.after_2 = arrow.Arrow( + 2016, 11, 6, 23, 59, tzinfo=tz.gettz("America/New_York") + ) + request.cls.before_3 = arrow.Arrow( + 2018, 11, 4, 3, 59, tzinfo=tz.gettz("America/New_York") + ) + request.cls.before_4 = arrow.Arrow(2018, 11, 4, tzinfo=tz.gettz("America/New_York")) + request.cls.after_3 = arrow.Arrow(2018, 11, 4, 4, tzinfo=tz.gettz("America/New_York")) + request.cls.after_4 = arrow.Arrow( + 2018, 11, 4, 23, 59, tzinfo=tz.gettz("America/New_York") + ) + + class TestTestArrowInit: def test_init_bad_input(self): @@ -184,11 +216,8 @@ def test_strptime(self): ) +@pytest.mark.usefixtures('time2013_02_03_fixture') class TestTestArrowRepresentation: - @classmethod - def setup_class(cls): - cls.arrow = arrow.Arrow(2013, 2, 3, 12, 30, 45, 1) - def test_repr(self): result = self.arrow.__repr__() @@ -232,6 +261,7 @@ def test_clone(self): assert result is not self.arrow assert result._datetime == self.arrow._datetime + @pytest.mark.usefixtures("time2013_01_01_fixture") class TestArrowAttribute: def test_getattr_base(self): @@ -346,6 +376,7 @@ def test_le(self): assert self.arrow <= self.arrow assert self.arrow <= self.arrow.datetime + @pytest.mark.usefixtures('time2013_01_01_fixture') class TestArrowMath: def test_add_timedelta(self): @@ -399,6 +430,7 @@ def test_rsub_other(self): with pytest.raises(TypeError): timedelta(days=1) - self.arrow + @pytest.mark.usefixtures("utcnow_fixture") class TestArrowDatetimeInterface: def test_date(self): @@ -500,6 +532,7 @@ def test_strftime(self): assert result == self.arrow._datetime.strftime("%Y") +@pytest.mark.usefixtures("timedst_fixture") class TestArrowFalsePositiveDst: """These tests relate to issues #376 and #551. The key points in both issues are that arrow will assign a UTC timezone if none is provided and @@ -524,27 +557,7 @@ class TestArrowFalsePositiveDst: 2018-11-04T02:00:00-05:00 """ - @classmethod - def setup_class(cls): - cls.before_1 = arrow.Arrow( - 2016, 11, 6, 3, 59, tzinfo=tz.gettz("America/New_York") - ) - cls.before_2 = arrow.Arrow(2016, 11, 6, tzinfo=tz.gettz("America/New_York")) - cls.after_1 = arrow.Arrow(2016, 11, 6, 4, tzinfo=tz.gettz("America/New_York")) - cls.after_2 = arrow.Arrow( - 2016, 11, 6, 23, 59, tzinfo=tz.gettz("America/New_York") - ) - cls.before_3 = arrow.Arrow( - 2018, 11, 4, 3, 59, tzinfo=tz.gettz("America/New_York") - ) - cls.before_4 = arrow.Arrow(2018, 11, 4, tzinfo=tz.gettz("America/New_York")) - cls.after_3 = arrow.Arrow(2018, 11, 4, 4, tzinfo=tz.gettz("America/New_York")) - cls.after_4 = arrow.Arrow( - 2018, 11, 4, 23, 59, tzinfo=tz.gettz("America/New_York") - ) - def test_dst(self): - assert self.before_1.day == self.before_2.day assert self.after_1.day == self.after_2.day assert self.before_3.day == self.before_4.day @@ -1194,12 +1207,8 @@ def test_bounds_param_is_passed(self): ] +@pytest.mark.usefixtures("time2013_02_15_fixture") class TestArrowSpan: - @classmethod - def setup_class(cls): - cls.datetime = datetime(2013, 2, 15, 3, 41, 22, 8923) - cls.arrow = arrow.Arrow.fromdatetime(cls.datetime) - def test_span_attribute(self): with pytest.raises(AttributeError): @@ -1315,6 +1324,7 @@ def test_bounds_are_validated(self): with pytest.raises(AttributeError): floor, ceil = self.arrow.span("hour", bounds="][") + @pytest.mark.usefixtures("time2013_01_01_fixture") class TestArrowHumanize: def test_granularity(self): @@ -1684,6 +1694,7 @@ def test_untranslated_granularity(self, mocker): with pytest.raises(ValueError): arw.humanize(later, granularity="week") + @pytest.mark.usefixtures("time2013_01_01_fixture") class TestArrowHumanizeTestsWithLocale: def test_now(self): From 9175306f535052c7df20e34ab5663fd86bc9ddd8 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Sun, 29 Mar 2020 01:35:25 +0000 Subject: [PATCH 399/649] Fixing up lint issues. --- tests/test_arrow.py | 12 ++++++++---- tests/test_factory.py | 2 +- tests/test_formatter.py | 3 +-- tests/test_locales.py | 9 +++++---- tests/test_parser.py | 3 +-- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 9b273e68c..5ac35a0bd 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -47,7 +47,9 @@ def timedst_fixture(request): 2016, 11, 6, 3, 59, tzinfo=tz.gettz("America/New_York") ) request.cls.before_2 = arrow.Arrow(2016, 11, 6, tzinfo=tz.gettz("America/New_York")) - request.cls.after_1 = arrow.Arrow(2016, 11, 6, 4, tzinfo=tz.gettz("America/New_York")) + request.cls.after_1 = arrow.Arrow( + 2016, 11, 6, 4, tzinfo=tz.gettz("America/New_York") + ) request.cls.after_2 = arrow.Arrow( 2016, 11, 6, 23, 59, tzinfo=tz.gettz("America/New_York") ) @@ -55,7 +57,9 @@ def timedst_fixture(request): 2018, 11, 4, 3, 59, tzinfo=tz.gettz("America/New_York") ) request.cls.before_4 = arrow.Arrow(2018, 11, 4, tzinfo=tz.gettz("America/New_York")) - request.cls.after_3 = arrow.Arrow(2018, 11, 4, 4, tzinfo=tz.gettz("America/New_York")) + request.cls.after_3 = arrow.Arrow( + 2018, 11, 4, 4, tzinfo=tz.gettz("America/New_York") + ) request.cls.after_4 = arrow.Arrow( 2018, 11, 4, 23, 59, tzinfo=tz.gettz("America/New_York") ) @@ -216,7 +220,7 @@ def test_strptime(self): ) -@pytest.mark.usefixtures('time2013_02_03_fixture') +@pytest.mark.usefixtures("time2013_02_03_fixture") class TestTestArrowRepresentation: def test_repr(self): @@ -377,7 +381,7 @@ def test_le(self): assert self.arrow <= self.arrow.datetime -@pytest.mark.usefixtures('time2013_01_01_fixture') +@pytest.mark.usefixtures("time2013_01_01_fixture") class TestArrowMath: def test_add_timedelta(self): diff --git a/tests/test_factory.py b/tests/test_factory.py index d968ef810..45dda6bf2 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -3,6 +3,7 @@ from datetime import date, datetime import dateparser +import pytest from dateutil import tz from arrow import factory @@ -10,7 +11,6 @@ from .utils import assert_datetime_equality -import pytest @pytest.fixture(scope="class") def factory_fixture(request): diff --git a/tests/test_formatter.py b/tests/test_formatter.py index 3f19a6e3b..d1f5bd74b 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -2,6 +2,7 @@ import time from datetime import datetime +import pytest import pytz from dateutil import tz as dateutil_tz @@ -9,8 +10,6 @@ from .utils import make_full_tz_list -import pytest - @pytest.fixture(scope="class") def formatting_fixture(request): diff --git a/tests/test_locales.py b/tests/test_locales.py index 0c4966db2..8fbbfb1e2 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from arrow import arrow, locales - import pytest +from arrow import arrow, locales + @pytest.fixture(scope="class") def locales_fixture(request): @@ -14,8 +14,8 @@ def locales_fixture(request): @pytest.fixture(scope="class") def lang_locale_fixture(request): name = request.cls.__name__[4:] - if name == 'Locale': - request.cls.locale = locales.get_locale_by_class_name('EnglishLocale') + if name == "Locale": + request.cls.locale = locales.get_locale_by_class_name("EnglishLocale") else: request.cls.locale = locales.get_locale_by_class_name(name) @@ -23,6 +23,7 @@ def lang_locale_fixture(request): @pytest.mark.usefixtures("locales_fixture") class TestLocaleValidation: """Validate locales to ensure that translations are valid and complete""" + def test_locale_validation(self): for _, locale_cls in self.locales.items(): diff --git a/tests/test_parser.py b/tests/test_parser.py index 52f1868a5..95094d0ae 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -6,6 +6,7 @@ import time from datetime import datetime +import pytest from dateutil import tz import arrow @@ -15,8 +16,6 @@ from .utils import make_full_tz_list -import pytest - @pytest.fixture(scope="class") def parser_fixture(request): From bc54bf027f7587c513c7e69245dad617eff5e6d8 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Sun, 29 Mar 2020 01:39:52 +0000 Subject: [PATCH 400/649] lint fix up. --- arrow/locales.py | 1 + 1 file changed, 1 insertion(+) diff --git a/arrow/locales.py b/arrow/locales.py index c56b38934..c62d03724 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -21,6 +21,7 @@ def get_locale(name): return locale_cls() + def get_locale_by_class_name(name): """Returns an appropriate :class:`Locale ` corresponding to an locale class name. From 3afa5e5ab94bc763d8aff00bde5551f144ac6801 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Mon, 30 Mar 2020 22:33:19 +0000 Subject: [PATCH 401/649] Moving fixtures to conftest.py --- tests/conftest.py | 93 +++++++++++++++++++++++++++++++++++++++++ tests/test_arrow.py | 47 --------------------- tests/test_factory.py | 6 --- tests/test_formatter.py | 7 ---- tests/test_locales.py | 14 ------- tests/test_parser.py | 15 ------- 6 files changed, 93 insertions(+), 89 deletions(-) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..3794d9d10 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +from datetime import datetime + +import pytest +from dateutil import tz + +from arrow import arrow, factory, formatter, locales, parser + + +@pytest.fixture(scope="class") +def utcnow_fixture(request): + request.cls.arrow = arrow.Arrow.utcnow() + + +@pytest.fixture(scope="class") +def time2013_01_01_fixture(request): + request.cls.now = arrow.Arrow.utcnow() + request.cls.arrow = arrow.Arrow(2013, 1, 1) + request.cls.datetime = datetime(2013, 1, 1) + + +@pytest.fixture(scope="class") +def time2013_02_03_fixture(request): + request.cls.arrow = arrow.Arrow(2013, 2, 3, 12, 30, 45, 1) + + +@pytest.fixture(scope="class") +def time2013_02_15_fixture(request): + request.cls.datetime = datetime(2013, 2, 15, 3, 41, 22, 8923) + request.cls.arrow = arrow.Arrow.fromdatetime(request.cls.datetime) + + +@pytest.fixture(scope="class") +def timedst_fixture(request): + request.cls.before_1 = arrow.Arrow( + 2016, 11, 6, 3, 59, tzinfo=tz.gettz("America/New_York") + ) + request.cls.before_2 = arrow.Arrow(2016, 11, 6, tzinfo=tz.gettz("America/New_York")) + request.cls.after_1 = arrow.Arrow( + 2016, 11, 6, 4, tzinfo=tz.gettz("America/New_York") + ) + request.cls.after_2 = arrow.Arrow( + 2016, 11, 6, 23, 59, tzinfo=tz.gettz("America/New_York") + ) + request.cls.before_3 = arrow.Arrow( + 2018, 11, 4, 3, 59, tzinfo=tz.gettz("America/New_York") + ) + request.cls.before_4 = arrow.Arrow(2018, 11, 4, tzinfo=tz.gettz("America/New_York")) + request.cls.after_3 = arrow.Arrow( + 2018, 11, 4, 4, tzinfo=tz.gettz("America/New_York") + ) + request.cls.after_4 = arrow.Arrow( + 2018, 11, 4, 23, 59, tzinfo=tz.gettz("America/New_York") + ) + + +@pytest.fixture(scope="class") +def formatting_fixture(request): + request.cls.formatter = formatter.DateTimeFormatter() + + +@pytest.fixture(scope="class") +def locales_fixture(request): + request.cls.locales = locales._locales + + +@pytest.fixture(scope="class") +def lang_locale_fixture(request): + name = request.cls.__name__[4:] + if name == "Locale": + request.cls.locale = locales.get_locale_by_class_name("EnglishLocale") + else: + request.cls.locale = locales.get_locale_by_class_name(name) + + +@pytest.fixture(scope="class") +def factory_fixture(request): + request.cls.factory = factory.ArrowFactory() + + +@pytest.fixture(scope="class") +def parser_fixture(request): + request.cls.parser = parser.DateTimeParser() + + +@pytest.fixture(scope="class") +def regex_fixture(request): + request.cls.format_regex = parser.DateTimeParser._FORMAT_RE + + +@pytest.fixture(scope="class") +def tzinfo_fixture(request): + request.cls.parser = parser.TzinfoParser() diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 5ac35a0bd..fb2c4d4a4 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -18,53 +18,6 @@ from .utils import assert_datetime_equality -@pytest.fixture(scope="class") -def utcnow_fixture(request): - request.cls.arrow = arrow.Arrow.utcnow() - - -@pytest.fixture(scope="class") -def time2013_01_01_fixture(request): - request.cls.now = arrow.Arrow.utcnow() - request.cls.arrow = arrow.Arrow(2013, 1, 1) - request.cls.datetime = datetime(2013, 1, 1) - - -@pytest.fixture(scope="class") -def time2013_02_03_fixture(request): - request.cls.arrow = arrow.Arrow(2013, 2, 3, 12, 30, 45, 1) - - -@pytest.fixture(scope="class") -def time2013_02_15_fixture(request): - request.cls.datetime = datetime(2013, 2, 15, 3, 41, 22, 8923) - request.cls.arrow = arrow.Arrow.fromdatetime(request.cls.datetime) - - -@pytest.fixture(scope="class") -def timedst_fixture(request): - request.cls.before_1 = arrow.Arrow( - 2016, 11, 6, 3, 59, tzinfo=tz.gettz("America/New_York") - ) - request.cls.before_2 = arrow.Arrow(2016, 11, 6, tzinfo=tz.gettz("America/New_York")) - request.cls.after_1 = arrow.Arrow( - 2016, 11, 6, 4, tzinfo=tz.gettz("America/New_York") - ) - request.cls.after_2 = arrow.Arrow( - 2016, 11, 6, 23, 59, tzinfo=tz.gettz("America/New_York") - ) - request.cls.before_3 = arrow.Arrow( - 2018, 11, 4, 3, 59, tzinfo=tz.gettz("America/New_York") - ) - request.cls.before_4 = arrow.Arrow(2018, 11, 4, tzinfo=tz.gettz("America/New_York")) - request.cls.after_3 = arrow.Arrow( - 2018, 11, 4, 4, tzinfo=tz.gettz("America/New_York") - ) - request.cls.after_4 = arrow.Arrow( - 2018, 11, 4, 23, 59, tzinfo=tz.gettz("America/New_York") - ) - - class TestTestArrowInit: def test_init_bad_input(self): diff --git a/tests/test_factory.py b/tests/test_factory.py index 45dda6bf2..b4bb3ca92 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -6,17 +6,11 @@ import pytest from dateutil import tz -from arrow import factory from arrow.parser import ParserError from .utils import assert_datetime_equality -@pytest.fixture(scope="class") -def factory_fixture(request): - request.cls.factory = factory.ArrowFactory() - - @pytest.mark.usefixtures("factory_fixture") class TestGet: def test_no_args(self): diff --git a/tests/test_formatter.py b/tests/test_formatter.py index d1f5bd74b..9303c4f70 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -6,16 +6,9 @@ import pytz from dateutil import tz as dateutil_tz -from arrow import formatter - from .utils import make_full_tz_list -@pytest.fixture(scope="class") -def formatting_fixture(request): - request.cls.formatter = formatter.DateTimeFormatter() - - @pytest.mark.usefixtures("formatting_fixture") class TestDateTimeFormatterFormatToken: def test_format(self): diff --git a/tests/test_locales.py b/tests/test_locales.py index 8fbbfb1e2..c3fdb160c 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -6,20 +6,6 @@ from arrow import arrow, locales -@pytest.fixture(scope="class") -def locales_fixture(request): - request.cls.locales = locales._locales - - -@pytest.fixture(scope="class") -def lang_locale_fixture(request): - name = request.cls.__name__[4:] - if name == "Locale": - request.cls.locale = locales.get_locale_by_class_name("EnglishLocale") - else: - request.cls.locale = locales.get_locale_by_class_name(name) - - @pytest.mark.usefixtures("locales_fixture") class TestLocaleValidation: """Validate locales to ensure that translations are valid and complete""" diff --git a/tests/test_parser.py b/tests/test_parser.py index 95094d0ae..ac80b0334 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -17,21 +17,6 @@ from .utils import make_full_tz_list -@pytest.fixture(scope="class") -def parser_fixture(request): - request.cls.parser = parser.DateTimeParser() - - -@pytest.fixture(scope="class") -def regex_fixture(request): - request.cls.format_regex = parser.DateTimeParser._FORMAT_RE - - -@pytest.fixture(scope="class") -def tzinfo_fixture(request): - request.cls.parser = parser.TzinfoParser() - - @pytest.mark.usefixtures("parser_fixture") class TestDateTimeParser: def test_parse_multiformat(self, mocker): From 6c5936b9b3c113332f8aa3cb5d667b2d3f234ed7 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Tue, 31 Mar 2020 00:15:03 +0000 Subject: [PATCH 402/649] Removing timedst_fixture for brevity. --- tests/conftest.py | 25 ------------------------- tests/test_arrow.py | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3794d9d10..a200945d3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,6 @@ from datetime import datetime import pytest -from dateutil import tz from arrow import arrow, factory, formatter, locales, parser @@ -30,30 +29,6 @@ def time2013_02_15_fixture(request): request.cls.arrow = arrow.Arrow.fromdatetime(request.cls.datetime) -@pytest.fixture(scope="class") -def timedst_fixture(request): - request.cls.before_1 = arrow.Arrow( - 2016, 11, 6, 3, 59, tzinfo=tz.gettz("America/New_York") - ) - request.cls.before_2 = arrow.Arrow(2016, 11, 6, tzinfo=tz.gettz("America/New_York")) - request.cls.after_1 = arrow.Arrow( - 2016, 11, 6, 4, tzinfo=tz.gettz("America/New_York") - ) - request.cls.after_2 = arrow.Arrow( - 2016, 11, 6, 23, 59, tzinfo=tz.gettz("America/New_York") - ) - request.cls.before_3 = arrow.Arrow( - 2018, 11, 4, 3, 59, tzinfo=tz.gettz("America/New_York") - ) - request.cls.before_4 = arrow.Arrow(2018, 11, 4, tzinfo=tz.gettz("America/New_York")) - request.cls.after_3 = arrow.Arrow( - 2018, 11, 4, 4, tzinfo=tz.gettz("America/New_York") - ) - request.cls.after_4 = arrow.Arrow( - 2018, 11, 4, 23, 59, tzinfo=tz.gettz("America/New_York") - ) - - @pytest.fixture(scope="class") def formatting_fixture(request): request.cls.formatter = formatter.DateTimeFormatter() diff --git a/tests/test_arrow.py b/tests/test_arrow.py index fb2c4d4a4..eb49e6629 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -489,7 +489,6 @@ def test_strftime(self): assert result == self.arrow._datetime.strftime("%Y") -@pytest.mark.usefixtures("timedst_fixture") class TestArrowFalsePositiveDst: """These tests relate to issues #376 and #551. The key points in both issues are that arrow will assign a UTC timezone if none is provided and @@ -515,6 +514,22 @@ class TestArrowFalsePositiveDst: """ def test_dst(self): + self.before_1 = arrow.Arrow( + 2016, 11, 6, 3, 59, tzinfo=tz.gettz("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.after_2 = arrow.Arrow( + 2016, 11, 6, 23, 59, tzinfo=tz.gettz("America/New_York") + ) + self.before_3 = arrow.Arrow( + 2018, 11, 4, 3, 59, tzinfo=tz.gettz("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.after_4 = arrow.Arrow( + 2018, 11, 4, 23, 59, tzinfo=tz.gettz("America/New_York") + ) assert self.before_1.day == self.before_2.day assert self.after_1.day == self.after_2.day assert self.before_3.day == self.before_4.day From 0f590b730cb51902e9ee80c5bb356e5106d501bb Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Tue, 31 Mar 2020 00:17:26 +0000 Subject: [PATCH 403/649] Merging TestLocale into TestEnglishLocale. --- tests/conftest.py | 7 +++---- tests/test_locales.py | 13 +++++-------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a200945d3..e93a6166d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -41,11 +41,10 @@ def locales_fixture(request): @pytest.fixture(scope="class") def lang_locale_fixture(request): + # As locale test classes are prefixed with Test, we are dynamically getting the locale by the test class name. + # TestEnglishLocale -> EnglishLocale name = request.cls.__name__[4:] - if name == "Locale": - request.cls.locale = locales.get_locale_by_class_name("EnglishLocale") - else: - request.cls.locale = locales.get_locale_by_class_name(name) + request.cls.locale = locales.get_locale_by_class_name(name) @pytest.fixture(scope="class") diff --git a/tests/test_locales.py b/tests/test_locales.py index c3fdb160c..1c8f705aa 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -59,7 +59,11 @@ def test_locales(self): @pytest.mark.usefixtures("lang_locale_fixture") -class TestLocale: +class TestEnglishLocale: + def test_describe(self): + assert self.locale.describe("now", only_distance=True) == "instantly" + assert self.locale.describe("now", only_distance=False) == "just now" + def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 2) == "2 hours" @@ -120,13 +124,6 @@ def test_meridian_invalid_token(self): assert self.locale.meridian(7, "NONSENSE") is None -@pytest.mark.usefixtures("lang_locale_fixture") -class TestEnglishLocale: - def test_describe(self): - assert self.locale.describe("now", only_distance=True) == "instantly" - assert self.locale.describe("now", only_distance=False) == "just now" - - @pytest.mark.usefixtures("lang_locale_fixture") class TestItalianLocale: def test_ordinal_number(self): From 76663fc190d92535c7a7fe48a6e26b69bb1704f9 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Tue, 31 Mar 2020 00:43:37 +0000 Subject: [PATCH 404/649] Removing _fixture in fixture naming and renaming to be more straightforward. --- tests/conftest.py | 30 +++++++++---------- tests/test_arrow.py | 16 +++++------ tests/test_factory.py | 6 ++-- tests/test_formatter.py | 2 +- tests/test_locales.py | 64 ++++++++++++++++++++--------------------- tests/test_parser.py | 18 ++++++------ 6 files changed, 68 insertions(+), 68 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e93a6166d..7d629758e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,40 +7,45 @@ @pytest.fixture(scope="class") -def utcnow_fixture(request): +def time_utcnow(request): request.cls.arrow = arrow.Arrow.utcnow() @pytest.fixture(scope="class") -def time2013_01_01_fixture(request): +def time_2013_01_01(request): request.cls.now = arrow.Arrow.utcnow() request.cls.arrow = arrow.Arrow(2013, 1, 1) request.cls.datetime = datetime(2013, 1, 1) @pytest.fixture(scope="class") -def time2013_02_03_fixture(request): +def time_2013_02_03(request): request.cls.arrow = arrow.Arrow(2013, 2, 3, 12, 30, 45, 1) @pytest.fixture(scope="class") -def time2013_02_15_fixture(request): +def time_2013_02_15(request): request.cls.datetime = datetime(2013, 2, 15, 3, 41, 22, 8923) request.cls.arrow = arrow.Arrow.fromdatetime(request.cls.datetime) @pytest.fixture(scope="class") -def formatting_fixture(request): +def arrow_formatter(request): request.cls.formatter = formatter.DateTimeFormatter() @pytest.fixture(scope="class") -def locales_fixture(request): +def arrow_factory(request): + request.cls.factory = factory.ArrowFactory() + + +@pytest.fixture(scope="class") +def lang_locales(request): request.cls.locales = locales._locales @pytest.fixture(scope="class") -def lang_locale_fixture(request): +def lang_locale(request): # As locale test classes are prefixed with Test, we are dynamically getting the locale by the test class name. # TestEnglishLocale -> EnglishLocale name = request.cls.__name__[4:] @@ -48,20 +53,15 @@ def lang_locale_fixture(request): @pytest.fixture(scope="class") -def factory_fixture(request): - request.cls.factory = factory.ArrowFactory() - - -@pytest.fixture(scope="class") -def parser_fixture(request): +def dt_parser(request): request.cls.parser = parser.DateTimeParser() @pytest.fixture(scope="class") -def regex_fixture(request): +def dt_regex(request): request.cls.format_regex = parser.DateTimeParser._FORMAT_RE @pytest.fixture(scope="class") -def tzinfo_fixture(request): +def dt_tzinfo(request): request.cls.parser = parser.TzinfoParser() diff --git a/tests/test_arrow.py b/tests/test_arrow.py index eb49e6629..74490c6d7 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -173,7 +173,7 @@ def test_strptime(self): ) -@pytest.mark.usefixtures("time2013_02_03_fixture") +@pytest.mark.usefixtures("time_2013_02_03") class TestTestArrowRepresentation: def test_repr(self): @@ -219,7 +219,7 @@ def test_clone(self): assert result._datetime == self.arrow._datetime -@pytest.mark.usefixtures("time2013_01_01_fixture") +@pytest.mark.usefixtures("time_2013_01_01") class TestArrowAttribute: def test_getattr_base(self): @@ -277,7 +277,7 @@ def test_float_timestamp(self): assert result == self.arrow.microsecond -@pytest.mark.usefixtures("utcnow_fixture") +@pytest.mark.usefixtures("time_utcnow") class TestArrowComparison: def test_eq(self): @@ -334,7 +334,7 @@ def test_le(self): assert self.arrow <= self.arrow.datetime -@pytest.mark.usefixtures("time2013_01_01_fixture") +@pytest.mark.usefixtures("time_2013_01_01") class TestArrowMath: def test_add_timedelta(self): @@ -388,7 +388,7 @@ def test_rsub_other(self): timedelta(days=1) - self.arrow -@pytest.mark.usefixtures("utcnow_fixture") +@pytest.mark.usefixtures("time_utcnow") class TestArrowDatetimeInterface: def test_date(self): @@ -1179,7 +1179,7 @@ def test_bounds_param_is_passed(self): ] -@pytest.mark.usefixtures("time2013_02_15_fixture") +@pytest.mark.usefixtures("time_2013_02_15") class TestArrowSpan: def test_span_attribute(self): @@ -1297,7 +1297,7 @@ def test_bounds_are_validated(self): floor, ceil = self.arrow.span("hour", bounds="][") -@pytest.mark.usefixtures("time2013_01_01_fixture") +@pytest.mark.usefixtures("time_2013_01_01") class TestArrowHumanize: def test_granularity(self): @@ -1667,7 +1667,7 @@ def test_untranslated_granularity(self, mocker): arw.humanize(later, granularity="week") -@pytest.mark.usefixtures("time2013_01_01_fixture") +@pytest.mark.usefixtures("time_2013_01_01") class TestArrowHumanizeTestsWithLocale: def test_now(self): diff --git a/tests/test_factory.py b/tests/test_factory.py index b4bb3ca92..02c6b82fc 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -11,7 +11,7 @@ from .utils import assert_datetime_equality -@pytest.mark.usefixtures("factory_fixture") +@pytest.mark.usefixtures("arrow_factory") class TestGet: def test_no_args(self): @@ -329,7 +329,7 @@ def test_locale_with_tzinfo(self): assert res.tzinfo == tz.gettz("Asia/Tokyo") -@pytest.mark.usefixtures("factory_fixture") +@pytest.mark.usefixtures("arrow_factory") class TestUtcNow: def test_utcnow(self): @@ -339,7 +339,7 @@ def test_utcnow(self): ) -@pytest.mark.usefixtures("factory_fixture") +@pytest.mark.usefixtures("arrow_factory") class TestNow: def test_no_tz(self): diff --git a/tests/test_formatter.py b/tests/test_formatter.py index 9303c4f70..c6d18c91e 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -9,7 +9,7 @@ from .utils import make_full_tz_list -@pytest.mark.usefixtures("formatting_fixture") +@pytest.mark.usefixtures("arrow_formatter") class TestDateTimeFormatterFormatToken: def test_format(self): diff --git a/tests/test_locales.py b/tests/test_locales.py index 1c8f705aa..99889e365 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -6,7 +6,7 @@ from arrow import arrow, locales -@pytest.mark.usefixtures("locales_fixture") +@pytest.mark.usefixtures("lang_locales") class TestLocaleValidation: """Validate locales to ensure that translations are valid and complete""" @@ -58,7 +58,7 @@ def test_locales(self): assert len(locales._locales) > 0 -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestEnglishLocale: def test_describe(self): assert self.locale.describe("now", only_distance=True) == "instantly" @@ -124,13 +124,13 @@ def test_meridian_invalid_token(self): assert self.locale.meridian(7, "NONSENSE") is None -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestItalianLocale: def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1º" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestSpanishLocale: def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1º" @@ -183,14 +183,14 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("years", -14) == "14 años" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestFrenchLocale: def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1er" assert self.locale.ordinal_number(2) == "2e" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestRussianLocale: def test_plurals2(self): assert self.locale._format_timeframe("hours", 0) == "0 часов" @@ -213,7 +213,7 @@ def test_plurals2(self): assert self.locale._format_timeframe("minutes", 25) == "25 минут" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestPolishLocale: def test_plurals(self): assert self.locale._format_timeframe("hours", 0) == "0 godzin" @@ -226,7 +226,7 @@ def test_plurals(self): assert self.locale._format_timeframe("hours", 25) == "25 godzin" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestIcelandicLocale: def test_format_timeframe(self): @@ -238,7 +238,7 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("now", 0) == "rétt í þessu" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestMalayalamLocale: def test_format_timeframe(self): @@ -262,7 +262,7 @@ def test_format_relative_future(self): assert result == "ഒരു മണിക്കൂർ മുമ്പ്" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestHindiLocale: def test_format_timeframe(self): @@ -285,7 +285,7 @@ def test_format_relative_future(self): assert result == "एक घंटा पहले" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestCzechLocale: def test_format_timeframe(self): @@ -312,7 +312,7 @@ def test_format_relative_past(self): assert result == "Před hodinou" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestSlovakLocale: def test_format_timeframe(self): @@ -339,7 +339,7 @@ def test_format_relative_past(self): assert result == "Pred hodinou" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestBulgarianLocale: def test_plurals2(self): assert self.locale._format_timeframe("hours", 0) == "0 часа" @@ -362,7 +362,7 @@ def test_plurals2(self): assert self.locale._format_timeframe("minutes", 25) == "25 минути" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestMacedonianLocale: def test_plurals_mk(self): # time @@ -389,7 +389,7 @@ def test_plurals_mk(self): assert self.locale._format_timeframe("minutes", 25) == "25 минути" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestHebrewLocale: def test_couple_of_timeframe(self): assert self.locale._format_timeframe("hours", 2) == "שעתיים" @@ -402,7 +402,7 @@ def test_couple_of_timeframe(self): assert self.locale._format_timeframe("years", 5) == "5 שנים" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestMarathiLocale: def test_dateCoreFunctionality(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) @@ -432,7 +432,7 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestFinnishLocale: def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 2) == ("2 tuntia", "2 tunnin") @@ -454,7 +454,7 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1." -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestGermanLocale: def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1." @@ -472,7 +472,7 @@ def test_define(self): assert self.locale.describe("year", only_distance=False) == "in einem Jahr" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestHungarianLocale: def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 2) == "2 óra" @@ -481,7 +481,7 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("now", 0) == "éppen most" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestEsperantoLocale: def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 2) == "2 horoj" @@ -493,7 +493,7 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1a" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestThaiLocale: def test_year_full(self): assert self.locale.year_full(2015) == "2558" @@ -518,7 +518,7 @@ def test_format_relative_future(self): assert result == "1 ชั่วโมง ที่ผ่านมา" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestBengaliLocale: def test_ordinal_number(self): assert self.locale._ordinal_number(0) == "0তম" @@ -533,7 +533,7 @@ def test_ordinal_number(self): assert self.locale._ordinal_number(-1) is None -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestSwissLocale: def test_ordinal_number(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) @@ -543,7 +543,7 @@ def test_ordinal_number(self): assert self.locale.day_abbreviation(dt.isoweekday()) == "Sa" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestRomanianLocale: def test_timeframes(self): @@ -578,7 +578,7 @@ def test_relative_timeframes(self): assert self.locale._format_relative("o zi", "day", 1) == "peste o zi" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestArabicLocale: def test_timeframes(self): @@ -611,7 +611,7 @@ def test_timeframes(self): assert self.locale._format_timeframe("years", 115) == "115 سنة" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestNepaliLocale: def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 3) == "3 घण्टा" @@ -630,7 +630,7 @@ def test_format_relative_past(self): assert result == "एक घण्टा पहिले" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestIndonesianLocale: def test_timeframes(self): assert self.locale._format_timeframe("hours", 2) == "2 jam" @@ -655,7 +655,7 @@ def test_format_relative_future(self): assert self.locale._format_relative("1 jam", "hour", -1) == "1 jam yang lalu" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestTagalogLocale: def test_format_timeframe(self): @@ -691,7 +691,7 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(114) == "ika-114" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestEstonianLocale: def test_format_timeframe(self): assert self.locale._format_timeframe("now", 0) == "just nüüd" @@ -735,7 +735,7 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("years", -14) == "14 aastat" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestPortugueseLocale: def test_format_timeframe(self): assert self.locale._format_timeframe("now", 0) == "agora" @@ -753,7 +753,7 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("years", 12) == "12 anos" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestBrazilianPortugueseLocale: def test_format_timeframe(self): assert self.locale._format_timeframe("now", 0) == "agora" @@ -771,7 +771,7 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("years", 12) == "12 anos" -@pytest.mark.usefixtures("lang_locale_fixture") +@pytest.mark.usefixtures("lang_locale") class TestHongKongLocale: def test_format_timeframe(self): assert self.locale._format_timeframe("now", 0) == "剛才" diff --git a/tests/test_parser.py b/tests/test_parser.py index ac80b0334..dc8d43b52 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -17,7 +17,7 @@ from .utils import make_full_tz_list -@pytest.mark.usefixtures("parser_fixture") +@pytest.mark.usefixtures("dt_parser") class TestDateTimeParser: def test_parse_multiformat(self, mocker): mocker.patch( @@ -165,7 +165,7 @@ def test_timestamp_format_list(self): self.parser.parse("15 Jul", "X") -@pytest.mark.usefixtures("parser_fixture") +@pytest.mark.usefixtures("dt_parser") class TestDateTimeParserParse: def test_parse_list(self, mocker): @@ -653,7 +653,7 @@ def test_parse_W(self): self.parser.parse(fmt, "W") -@pytest.mark.usefixtures("regex_fixture") +@pytest.mark.usefixtures("dt_regex") class TestDateTimeParserRegex: def test_format_year(self): @@ -801,7 +801,7 @@ def test_time(self): assert time_re.findall("12:35:46,") == [] -@pytest.mark.usefixtures("parser_fixture") +@pytest.mark.usefixtures("dt_parser") class TestDateTimeParserISO: def test_YYYY(self): @@ -1205,7 +1205,7 @@ def test_midnight_end_day(self): self.parser.parse_iso("2019-12-31T24:00:00.999999") -@pytest.mark.usefixtures("tzinfo_fixture") +@pytest.mark.usefixtures("dt_tzinfo") class TestTzinfoParser: def test_parse_local(self): @@ -1241,7 +1241,7 @@ def test_parse_fails(self): self.parser.parse("fail") -@pytest.mark.usefixtures("parser_fixture") +@pytest.mark.usefixtures("dt_parser") class TestDateTimeParserMonthName: def test_shortmonth_capitalized(self): @@ -1284,7 +1284,7 @@ def test_localized_month_abbreviation(self): assert parser_.parse("2013-Gen-01", "YYYY-MMM-DD") == datetime(2013, 1, 1) -@pytest.mark.usefixtures("parser_fixture") +@pytest.mark.usefixtures("dt_parser") class TestDateTimeParserMeridians: def test_meridians_lowercase(self): assert self.parser.parse("2013-01-01 5am", "YYYY-MM-DD ha") == datetime( @@ -1345,7 +1345,7 @@ def test_fr_meridians(self): parser_.parse("Janvier 30, 2019 - 08:00 pm", "MMMM DD, YYYY - hh:mm a") -@pytest.mark.usefixtures("parser_fixture") +@pytest.mark.usefixtures("dt_parser") class TestDateTimeParserMonthOrdinalDay: def test_english(self): parser_ = parser.DateTimeParser("en_us") @@ -1412,7 +1412,7 @@ def test_french(self): ) -@pytest.mark.usefixtures("parser_fixture") +@pytest.mark.usefixtures("dt_parser") class TestDateTimeParserSearchDate: def test_parse_search(self): From 7acd6eb206d067c7e4148ce5b32b93afb88bcb17 Mon Sep 17 00:00:00 2001 From: "Kristijan \"Fremen\" Velkovski" Date: Tue, 31 Mar 2020 13:37:07 +0000 Subject: [PATCH 405/649] Changing fixture names. --- tests/conftest.py | 4 ++-- tests/test_parser.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7d629758e..7b53743d3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -58,10 +58,10 @@ def dt_parser(request): @pytest.fixture(scope="class") -def dt_regex(request): +def dt_parser_regex(request): request.cls.format_regex = parser.DateTimeParser._FORMAT_RE @pytest.fixture(scope="class") -def dt_tzinfo(request): +def tzinfo_parser(request): request.cls.parser = parser.TzinfoParser() diff --git a/tests/test_parser.py b/tests/test_parser.py index dc8d43b52..ae19b8429 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -653,7 +653,7 @@ def test_parse_W(self): self.parser.parse(fmt, "W") -@pytest.mark.usefixtures("dt_regex") +@pytest.mark.usefixtures("dt_parser_regex") class TestDateTimeParserRegex: def test_format_year(self): @@ -1205,7 +1205,7 @@ def test_midnight_end_day(self): self.parser.parse_iso("2019-12-31T24:00:00.999999") -@pytest.mark.usefixtures("dt_tzinfo") +@pytest.mark.usefixtures("tzinfo_parser") class TestTzinfoParser: def test_parse_local(self): From 34213790ebbafb743ddcc8fb15d051bc9bd44249 Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Wed, 1 Apr 2020 23:10:42 +0100 Subject: [PATCH 406/649] Allow formatting with 'x' token (#766) * Allow formatting with 'x' token * Fix rounding error in test * Remove space --- arrow/formatter.py | 6 +++++- tests/test_formatter.py | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/arrow/formatter.py b/arrow/formatter.py index d29d5e880..2a76b3385 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -16,7 +16,7 @@ class DateTimeFormatter(object): # emulated in Python's re library, see https://stackoverflow.com/a/13577411/2701578 _FORMAT_RE = re.compile( - r"(\[(?:(?=(?P[^]]))(?P=literal))*\]|YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X|W)" + r"(\[(?:(?=(?P[^]]))(?P=literal))*\]|YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X|x|W)" ) def __init__(self, locale="en_us"): @@ -100,6 +100,10 @@ def _format_token(self, dt, token): if token == "X": return str(calendar.timegm(dt.utctimetuple())) + if token == "x": + ts = calendar.timegm(dt.utctimetuple()) + (dt.microsecond / 1000000) + return str(int(ts * 1000000)) + if token == "ZZZ": return dt.tzname() diff --git a/tests/test_formatter.py b/tests/test_formatter.py index 5ae484d6f..3d57a6170 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -109,6 +109,10 @@ def test_timestamp(self): dt = datetime.utcfromtimestamp(timestamp) assert self.formatter._format_token(dt, "X") == str(int(timestamp)) + # time.time() may return a float with greater than 6 digits of precision + rounded_ts = str(round(timestamp * 1000000)) + assert self.formatter._format_token(dt, "x") == rounded_ts.format("{f}") + def test_timezone(self): dt = datetime.utcnow().replace(tzinfo=dateutil_tz.gettz("US/Pacific")) From 9a1df216c4161b35f4ae26cb0b6500e8c3d5f1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20W=C3=B3dkiewicz?= Date: Tue, 7 Apr 2020 21:40:10 +0200 Subject: [PATCH 407/649] Add week timeframe to Polish locale (#769) * add week timeframes to Polish locale * fix Polish locale * fix lint errors Co-authored-by: Chris <30196510+systemcatch@users.noreply.github.com> --- arrow/locales.py | 16 ++++++++++------ tests/test_locales.py | 44 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index c62d03724..dbcfd95d8 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1242,20 +1242,24 @@ class PolishLocale(SlavicBaseLocale): past = "{0} temu" future = "za {0}" + # The nouns should be in genitive case (Polish: "dopełniacz") + # in order to correctly form `past` & `future` expressions. timeframes = { "now": "teraz", - "second": "sekunda", - "seconds": "{0} kilka sekund", + "second": "sekundę", + "seconds": ["{0} sekund", "{0} sekundy", "{0} sekund"], "minute": "minutę", "minutes": ["{0} minut", "{0} minuty", "{0} minut"], - "hour": "godzina", + "hour": "godzinę", "hours": ["{0} godzin", "{0} godziny", "{0} godzin"], "day": "dzień", - "days": ["{0} dzień", "{0} dni", "{0} dni"], + "days": "{0} dni", + "week": "tydzień", + "weeks": ["{0} tygodni", "{0} tygodnie", "{0} tygodni"], "month": "miesiąc", - "months": ["{0} miesiąc", "{0} miesiące", "{0} miesięcy"], + "months": ["{0} miesięcy", "{0} miesiące", "{0} miesięcy"], "year": "rok", - "years": ["{0} rok", "{0} lata", "{0} lat"], + "years": ["{0} lat", "{0} lata", "{0} lat"], } month_names = [ diff --git a/tests/test_locales.py b/tests/test_locales.py index 99889e365..28d19e907 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -216,15 +216,55 @@ 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" + assert self.locale._format_timeframe("seconds", 5) == "5 sekund" + assert self.locale._format_timeframe("seconds", 21) == "21 sekund" + assert self.locale._format_timeframe("seconds", 22) == "22 sekundy" + assert self.locale._format_timeframe("seconds", 25) == "25 sekund" + + assert self.locale._format_timeframe("minutes", 0) == "0 minut" + assert self.locale._format_timeframe("minute", 1) == "minutę" + assert self.locale._format_timeframe("minutes", 2) == "2 minuty" + assert self.locale._format_timeframe("minutes", 5) == "5 minut" + assert self.locale._format_timeframe("minutes", 21) == "21 minut" + assert self.locale._format_timeframe("minutes", 22) == "22 minuty" + assert self.locale._format_timeframe("minutes", 25) == "25 minut" + assert self.locale._format_timeframe("hours", 0) == "0 godzin" - assert self.locale._format_timeframe("hours", 1) == "1 godzin" + assert self.locale._format_timeframe("hour", 1) == "godzinę" assert self.locale._format_timeframe("hours", 2) == "2 godziny" - assert self.locale._format_timeframe("hours", 4) == "4 godziny" assert self.locale._format_timeframe("hours", 5) == "5 godzin" assert self.locale._format_timeframe("hours", 21) == "21 godzin" assert self.locale._format_timeframe("hours", 22) == "22 godziny" assert self.locale._format_timeframe("hours", 25) == "25 godzin" + assert self.locale._format_timeframe("weeks", 0) == "0 tygodni" + assert self.locale._format_timeframe("week", 1) == "tydzień" + assert self.locale._format_timeframe("weeks", 2) == "2 tygodnie" + assert self.locale._format_timeframe("weeks", 5) == "5 tygodni" + assert self.locale._format_timeframe("weeks", 21) == "21 tygodni" + assert self.locale._format_timeframe("weeks", 22) == "22 tygodnie" + assert self.locale._format_timeframe("weeks", 25) == "25 tygodni" + + assert self.locale._format_timeframe("months", 0) == "0 miesięcy" + assert self.locale._format_timeframe("month", 1) == "miesiąc" + assert self.locale._format_timeframe("months", 2) == "2 miesiące" + assert self.locale._format_timeframe("months", 5) == "5 miesięcy" + assert self.locale._format_timeframe("months", 21) == "21 miesięcy" + assert self.locale._format_timeframe("months", 22) == "22 miesiące" + assert self.locale._format_timeframe("months", 25) == "25 miesięcy" + + assert self.locale._format_timeframe("years", 0) == "0 lat" + assert self.locale._format_timeframe("year", 1) == "rok" + assert self.locale._format_timeframe("years", 2) == "2 lata" + assert self.locale._format_timeframe("years", 5) == "5 lat" + assert self.locale._format_timeframe("years", 21) == "21 lat" + assert self.locale._format_timeframe("years", 22) == "22 lata" + assert self.locale._format_timeframe("years", 25) == "25 lat" + @pytest.mark.usefixtures("lang_locale") class TestIcelandicLocale: From d6753353002704cdf86e65e59c7c835340723a82 Mon Sep 17 00:00:00 2001 From: Nils Philippsen Date: Sat, 11 Apr 2020 13:35:43 +0200 Subject: [PATCH 408/649] Consolidate German locales and add missing 'week'/'weeks' localization (#768) * Add week/weeks granularity to GermanLocale Signed-off-by: Nils Philippsen * Use English names for German locales consistently Signed-off-by: Nils Philippsen * Clarify why GermanBaseLocale.describe() is needed In the course, dispatch `only_distance == False` to the base class and only compute the special case when needed. Signed-off-by: Nils Philippsen * Consolidate SwissLocale into GermanBaseLocale SwissLocale was effectively a copy of GermanLocale, with some attributes missing. Both locales declared themselves responsible for 'de', making it dependent on chance which one was chosen for localizing. Having only one class responsible (i.e. the common base class) avoids having to maintain both when extending, e.g. as it happened when 'week'/'weeks' was added. Signed-off-by: Nils Philippsen * Fix get_locale_by_class_name() and test Previously, this would raise a KeyError rather than a ValueError if a non-existent class is requested. Additionally, add a test for this function. Signed-off-by: Nils Philippsen --- arrow/locales.py | 97 ++++++++----------------------------------- tests/test_locales.py | 33 ++++++++++----- 2 files changed, 41 insertions(+), 89 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index dbcfd95d8..36e07f90f 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -29,7 +29,7 @@ def get_locale_by_class_name(name): :param name: the name of the locale class. """ - locale_cls = globals()[name] + locale_cls = globals().get(name) if locale_cls is None: raise ValueError("Unsupported locale '{}'".format(name)) @@ -1653,7 +1653,7 @@ class MacedonianLocale(SlavicBaseLocale): ] -class DeutschBaseLocale(Locale): +class GermanBaseLocale(Locale): past = "vor {0}" future = "in {0}" @@ -1669,6 +1669,8 @@ class DeutschBaseLocale(Locale): "hours": "{0} Stunden", "day": "einem Tag", "days": "{0} Tagen", + "week": "einer Woche", + "weeks": "{0} Wochen", "month": "einem Monat", "months": "{0} Monaten", "year": "einem Jahr", @@ -1679,6 +1681,7 @@ class DeutschBaseLocale(Locale): timeframes_only_distance["minute"] = "eine Minute" timeframes_only_distance["hour"] = "eine Stunde" timeframes_only_distance["day"] = "ein Tag" + timeframes_only_distance["week"] = "eine Woche" timeframes_only_distance["month"] = "ein Monat" timeframes_only_distance["year"] = "ein Jahr" @@ -1738,21 +1741,28 @@ def describe(self, timeframe, delta=0, only_distance=False): :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords """ - humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) - if not only_distance: - humanized = self._format_timeframe(timeframe, delta) - humanized = self._format_relative(humanized, timeframe, delta) + return super(GermanBaseLocale, self).describe( + timeframe, delta, only_distance + ) + + # German uses a different case without 'in' or 'ago' + humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) return humanized -class GermanLocale(DeutschBaseLocale, Locale): +class GermanLocale(GermanBaseLocale, Locale): names = ["de", "de_de"] -class AustrianLocale(DeutschBaseLocale, Locale): +class SwissLocale(GermanBaseLocale, Locale): + + names = ["de_ch"] + + +class AustrianLocale(GermanBaseLocale, Locale): names = ["de_at"] @@ -3769,77 +3779,6 @@ class RomanshLocale(Locale): day_abbreviations = ["", "gli", "ma", "me", "gie", "ve", "so", "du"] -class SwissLocale(Locale): - - names = ["de", "de_ch"] - - past = "vor {0}" - future = "in {0}" - - timeframes = { - "now": "gerade eben", - "second": "eine Sekunde", - "seconds": "{0} Sekunden", - "minute": "einer Minute", - "minutes": "{0} Minuten", - "hour": "einer Stunde", - "hours": "{0} Stunden", - "day": "einem Tag", - "days": "{0} Tagen", - "week": "einer Woche", - "weeks": "{0} Wochen", - "month": "einem Monat", - "months": "{0} Monaten", - "year": "einem Jahr", - "years": "{0} Jahren", - } - - month_names = [ - "", - "Januar", - "Februar", - "März", - "April", - "Mai", - "Juni", - "Juli", - "August", - "September", - "Oktober", - "November", - "Dezember", - ] - - month_abbreviations = [ - "", - "Jan", - "Feb", - "Mär", - "Apr", - "Mai", - "Jun", - "Jul", - "Aug", - "Sep", - "Okt", - "Nov", - "Dez", - ] - - day_names = [ - "", - "Montag", - "Dienstag", - "Mittwoch", - "Donnerstag", - "Freitag", - "Samstag", - "Sonntag", - ] - - day_abbreviations = ["", "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] - - class RomanianLocale(Locale): names = ["ro", "ro_ro"] diff --git a/tests/test_locales.py b/tests/test_locales.py index 28d19e907..012576316 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -53,6 +53,22 @@ def test_get_locale(self, mocker): assert result == mock_locale + def test_get_locale_by_class_name(self, mocker): + mock_locale_cls = mocker.Mock() + mock_locale_obj = mock_locale_cls.return_value = mocker.Mock() + + globals_fn = mocker.Mock() + globals_fn.return_value = {"NonExistentLocale": mock_locale_cls} + + with pytest.raises(ValueError): + arrow.locales.get_locale_by_class_name("NonExistentLocale") + + mocker.patch.object(locales, "globals", globals_fn) + result = arrow.locales.get_locale_by_class_name("NonExistentLocale") + + mock_locale_cls.assert_called_once_with() + assert result == mock_locale_obj + def test_locales(self): assert len(locales._locales) > 0 @@ -506,11 +522,18 @@ def test_define(self): assert self.locale.describe("hour", only_distance=False) == "in einer Stunde" assert self.locale.describe("day", only_distance=True) == "ein Tag" assert self.locale.describe("day", only_distance=False) == "in einem Tag" + assert self.locale.describe("week", only_distance=True) == "eine Woche" + assert self.locale.describe("week", only_distance=False) == "in einer Woche" assert self.locale.describe("month", only_distance=True) == "ein Monat" assert self.locale.describe("month", only_distance=False) == "in einem Monat" assert self.locale.describe("year", only_distance=True) == "ein Jahr" assert self.locale.describe("year", only_distance=False) == "in einem Jahr" + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "Samstag" + assert self.locale.day_abbreviation(dt.isoweekday()) == "Sa" + @pytest.mark.usefixtures("lang_locale") class TestHungarianLocale: @@ -573,16 +596,6 @@ def test_ordinal_number(self): assert self.locale._ordinal_number(-1) is None -@pytest.mark.usefixtures("lang_locale") -class TestSwissLocale: - def test_ordinal_number(self): - dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) - - assert self.locale._format_timeframe("minute", 1) == "einer Minute" - assert self.locale._format_timeframe("hour", 1) == "einer Stunde" - assert self.locale.day_abbreviation(dt.isoweekday()) == "Sa" - - @pytest.mark.usefixtures("lang_locale") class TestRomanianLocale: def test_timeframes(self): From 210bace9e99d28cc6924ce3db9d2bc7358ae0256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Sun, 19 Apr 2020 18:24:57 +0200 Subject: [PATCH 409/649] Skip dateparser tests when the package is not installed Skip the dateparser test if dateparser is not installed. We do not want to package it for just one test. --- setup.cfg | 2 +- tests/test_factory.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 039cd09b8..71a519e11 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ addopts = -v --cov-branch --cov=arrow tests --cov-fail-under=100 --cov-report=te line_length = 88 multi_line_output = 3 include_trailing_comma = true -known_third_party = dateparser,dateutil,pytest,pytz,setuptools,simplejson +known_third_party = dateutil,pytest,pytz,setuptools,simplejson [flake8] per-file-ignores = arrow/__init__.py:F401 diff --git a/tests/test_factory.py b/tests/test_factory.py index 02c6b82fc..cd918c91a 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -2,7 +2,6 @@ import time from datetime import date, datetime -import dateparser import pytest from dateutil import tz @@ -113,6 +112,7 @@ def test_one_arg_tzinfo(self): # regression test for issue #658 def test_one_arg_dateparser_datetime(self): + dateparser = pytest.importorskip("dateparser") expected = datetime(1990, 1, 1).replace(tzinfo=tz.tzutc()) # dateparser outputs: datetime.datetime(1990, 1, 1, 0, 0, tzinfo=) parsed_date = dateparser.parse("1990-01-01T00:00:00+00:00") From c79b550b161c33c7c9607e87e8fbeee0756a76c3 Mon Sep 17 00:00:00 2001 From: Andy Date: Sat, 25 Apr 2020 18:34:42 +0800 Subject: [PATCH 410/649] Adds "week" locale support for zh_tw (#776) Also adds missing tests for timeframes --- arrow/locales.py | 9 ++++++--- tests/test_locales.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 36e07f90f..d33fb882b 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -908,17 +908,20 @@ class ChineseTWLocale(Locale): past = "{0}前" future = "{0}後" + and_word = "和" timeframes = { "now": "剛才", - "second": "一秒", - "seconds": "{0}幾秒", + "second": "1秒", + "seconds": "{0}秒", "minute": "1分鐘", "minutes": "{0}分鐘", "hour": "1小時", "hours": "{0}小時", "day": "1天", "days": "{0}天", + "week": "1週", + "weeks": "{0}週", "month": "1個月", "months": "{0}個月", "year": "1年", @@ -956,7 +959,7 @@ class ChineseTWLocale(Locale): "12", ] - day_names = ["", "周一", "周二", "周三", "周四", "周五", "周六", "周日"] + day_names = ["", "週一", "週二", "週三", "週四", "週五", "週六", "週日"] day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] diff --git a/tests/test_locales.py b/tests/test_locales.py index 012576316..0e749a829 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -842,3 +842,23 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("months", 11) == "11個月" assert self.locale._format_timeframe("year", 1) == "1年" assert self.locale._format_timeframe("years", 12) == "12年" + + +@pytest.mark.usefixtures("lang_locale") +class TestChineseTWLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "剛才" + assert self.locale._format_timeframe("second", 1) == "1秒" + assert self.locale._format_timeframe("seconds", 30) == "30秒" + assert self.locale._format_timeframe("minute", 1) == "1分鐘" + assert self.locale._format_timeframe("minutes", 40) == "40分鐘" + assert self.locale._format_timeframe("hour", 1) == "1小時" + assert self.locale._format_timeframe("hours", 23) == "23小時" + assert self.locale._format_timeframe("day", 1) == "1天" + assert self.locale._format_timeframe("days", 12) == "12天" + assert self.locale._format_timeframe("week", 1) == "1週" + assert self.locale._format_timeframe("weeks", 38) == "38週" + assert self.locale._format_timeframe("month", 1) == "1個月" + assert self.locale._format_timeframe("months", 11) == "11個月" + assert self.locale._format_timeframe("year", 1) == "1年" + assert self.locale._format_timeframe("years", 12) == "12年" From 3088a51c6e20489e8a4f9ce0f7ab115d261d9c62 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Wed, 29 Apr 2020 14:12:37 +0100 Subject: [PATCH 411/649] Bump version to 0.15.6 and update CHANGELOG --- CHANGELOG.rst | 32 ++++++++++++++++++++++++++++++++ arrow/_version.py | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 604bcd484..2524c9aad 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,38 @@ Changelog ========= +0.15.6 (2020-04-29) +------------------- + +- [NEW] Added support for parsing and formatting `ISO 8601 week dates `_ via a new token **W**, for example: + +.. code-block:: python + + >>> arrow.get("2013-W29-6", "W") + + >>> utc=arrow.utcnow() + >>> utc + + >>> utc.format("W") + '2020-W04-4' + +- [NEW] Formatting with ``x`` token (microseconds) is now possible, for example: + +.. code-block:: python + + >>> dt = arrow.utcnow() + >>> dt.format("x") + '1585669870688329' + >>> dt.format("X") + '1585669870' + +- [NEW] Added ``humanize`` week granularity translation for German, Italian, Polish & Taiwanese locales. +- [FIX] Consolidated and simplified German locales. +- [INTERNAL] Moved testing suite from Chai to pytest. +- [INTERNAL] Started using pytest fixtures for tests. +- [INTERNAL] Setup Github Actions for CI alongside Travis. +- [INTERNAL] You can now donate to arrow on `Open Collective `_. + 0.15.5 (2020-01-03) ------------------- diff --git a/arrow/_version.py b/arrow/_version.py index 32e2f393c..07c3e7b82 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.15.5" +__version__ = "0.15.6" From f5acf87d21d9d17e80f95d583efa46311f1cc7d6 Mon Sep 17 00:00:00 2001 From: systemcatch <30196510+systemcatch@users.noreply.github.com> Date: Thu, 30 Apr 2020 14:16:54 +0100 Subject: [PATCH 412/649] Small changes to wording --- CHANGELOG.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2524c9aad..a15914deb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,7 @@ Changelog 0.15.6 (2020-04-29) ------------------- -- [NEW] Added support for parsing and formatting `ISO 8601 week dates `_ via a new token **W**, for example: +- [NEW] Added support for parsing and formatting `ISO 8601 week dates `_ via a new token ``W``, for example: .. code-block:: python @@ -28,10 +28,10 @@ Changelog - [NEW] Added ``humanize`` week granularity translation for German, Italian, Polish & Taiwanese locales. - [FIX] Consolidated and simplified German locales. -- [INTERNAL] Moved testing suite from Chai to pytest. -- [INTERNAL] Started using pytest fixtures for tests. +- [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] You can now donate to arrow on `Open Collective `_. +- [INTERNAL] Help support Arrow's future development by donating to the project on `Open Collective `_. 0.15.5 (2020-01-03) ------------------- From 86d75aeca2266ba520ad7ad4ff56135a15b44da2 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 30 Apr 2020 17:14:55 -0400 Subject: [PATCH 413/649] Added better contributing docs to README and improved PR template --- .github/pull_request_template.md | 3 ++- README.rst | 10 +++++++++- tests/test_parser.py | 2 -- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 552b9a972..4ce59fcb5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,6 +5,7 @@ Thank you for taking the time to improve Arrow! Before submitting your pull requ - [ ] 🧪 Added **tests** for changed code. - [ ] 🛠️ All tests **pass** when run locally (run `tox` or `make test` to find out!). +- [ ] 🧹 All linting checks **pass** when run locally (run `tox -e lint` or `make lint` to find out!). - [ ] 📚 Updated **documentation** for changed code. - [ ] ⏩ Code is **up-to-date** with the `master` branch. @@ -13,7 +14,7 @@ If you have *any* questions about your code changes or any of the points above, ## Description of Changes ## Issue Description ## System Info -- 🖥 **OS name and version**: -- 🐍 **Python version**: -- 🏹 **Arrow version**: +- 🖥 **OS name and version**: +- 🐍 **Python version**: +- 🏹 **Arrow version**: diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md index c6b85c8c5..753ed0c62 100644 --- a/.github/ISSUE_TEMPLATE/documentation.md +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -1,5 +1,5 @@ --- -name: "📚 Documentation" +name: "📚 Documentation" about: Find errors or problems in the docs (https://arrow.readthedocs.io)? title: '' labels: 'documentation' @@ -14,4 +14,4 @@ Please provide us with a detailed description of the documentation issue. ## Issue Description - + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 4202e812c..fcab9213f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,5 +1,5 @@ --- -name: "💡 Feature Request" +name: "💡 Feature Request" about: Have an idea for a new feature or improvement? title: '' labels: 'enhancement' @@ -9,10 +9,9 @@ assignees: '' ## Feature Request - + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6cd1957d6..0e07c288a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,12 +2,12 @@ Thank you for taking the time to improve Arrow! Before submitting your pull request, please check all *appropriate* boxes: - -- [ ] 🧪 Added **tests** for changed code. -- [ ] 🛠️ All tests **pass** when run locally (run `tox` or `make test` to find out!). -- [ ] 🧹 All linting checks **pass** when run locally (run `tox -e lint` or `make lint` to find out!). -- [ ] 📚 Updated **documentation** for changed code. -- [ ] ⏩ Code is **up-to-date** with the `master` branch. + +- [ ] 🧪 Added **tests** for changed code. +- [ ] 🛠️ All tests **pass** when run locally (run `tox` or `make test` to find out!). +- [ ] 🧹 All linting checks **pass** when run locally (run `tox -e lint` or `make lint` to find out!). +- [ ] 📚 Updated **documentation** for changed code. +- [ ] ⏩ Code is **up-to-date** with the `master` branch. If you have *any* questions about your code changes or any of the points above, please submit your questions along with the pull request and we will try our best to help! @@ -18,7 +18,5 @@ Replace this commented text block with a description of your code changes. If your PR has an associated issue, insert the issue number (e.g. #703) or directly link to the GitHub issue (e.g. https://github.com/arrow-py/arrow/issues/703). -For example, doing the following will automatically close issue #703 when this PR is merged: - -Closes: #703 +Pro-tip: writing "Closes: #703" in the PR body will automatically close issue #703 when the PR is merged. --> From b746fedf7286c3755a46f07ab72f4c414cd41fc0 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 6 Oct 2020 16:39:36 -0400 Subject: [PATCH 470/649] Add release notes and bump version for 0.17.0 (#858) * Add release notes and bump version for 0.17.0 * Address comments --- CHANGELOG.rst | 36 ++++++++++++++++++++++++++++++++++-- arrow/_version.py | 2 +- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c7a779a6e..0b55a4522 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,10 +1,42 @@ Changelog ========= +0.17.0 (2020-10-2) +------------------- + +- [WARN] Arrow will **drop support** for Python 2.7 and 3.5 in the upcoming 1.0.0 release. This is the last major release to support Python 2.7 and Python 3.5. +- [NEW] Arrow now properly handles imaginary datetimes during DST shifts. For example: + +..code-block:: python + >>> just_before = arrow.get(2013, 3, 31, 1, 55, tzinfo="Europe/Paris") + >>> just_before.shift(minutes=+10) + + +..code-block:: python + >>> before = arrow.get("2018-03-10 23:00:00", "YYYY-MM-DD HH:mm:ss", tzinfo="US/Pacific") + >>> after = arrow.get("2018-03-11 04:00:00", "YYYY-MM-DD HH:mm:ss", tzinfo="US/Pacific") + >>> result=[(t, t.to("utc")) for t in arrow.Arrow.range("hour", before, after)] + >>> for r in result: + ... print(r) + ... + (, ) + (, ) + (, ) + (, ) + (, ) + +- [NEW] Added ``humanize`` week granularity translation for Tagalog. +- [CHANGE] Calls to the ``timestamp`` property now emit a ``DeprecationWarning``. In a future release, ``timestamp`` will be changed to a method to align with Python's datetime module. If you would like to continue using the property, please change your code to use the ``int_timestamp`` or ``float_timestamp`` properties instead. +- [CHANGE] Expanded and improved Catalan locale. +- [FIX] Fixed a bug that caused ``Arrow.range()`` to incorrectly cut off ranges in certain scenarios when using month, quarter, or year endings. +- [FIX] Fixed a bug that caused day of week token parsing to be case sensitive. +- [INTERNAL] A number of functions were reordered in arrow.py for better organization and grouping of related methods. This change will have no impact on usage. +- [INTERNAL] A minimum tox version is now enforced for compatibility reasons. Contributors must use tox >3.18.0 going forward. + 0.16.0 (2020-08-23) ------------------- -- [WARN] Arrow will **drop support** for Python 2.7 and 3.5 in the 1.0.0 release in late September. The 0.16.x and 0.17.x releases are the last to support Python 2.7 and 3.5. +- [WARN] Arrow will **drop support** for Python 2.7 and 3.5 in the upcoming 1.0.0 release. The 0.16.x and 0.17.x releases are the last to support Python 2.7 and 3.5. - [NEW] Implemented `PEP 495 `_ to handle ambiguous datetimes. This is achieved by the addition of the ``fold`` attribute for Arrow objects. For example: .. code-block:: python @@ -32,7 +64,7 @@ Changelog 0.15.8 (2020-07-23) ------------------- -- [WARN] Arrow will **drop support** for Python 2.7 and 3.5 in the 1.0.0 release in late September. The 0.15.x, 0.16.x, and 0.17.x releases are the last to support Python 2.7 and 3.5. +- [WARN] Arrow will **drop support** for Python 2.7 and 3.5 in the upcoming 1.0.0 release. The 0.15.x, 0.16.x, and 0.17.x releases are the last to support Python 2.7 and 3.5. - [NEW] Added ``humanize`` week granularity translation for Czech. - [FIX] ``arrow.get`` will now pick sane defaults when weekdays are passed with particular token combinations, see `#446 `_. - [INTERNAL] Moved arrow to an organization. The repo can now be found `here `_. diff --git a/arrow/_version.py b/arrow/_version.py index 5a313cc7e..fd86b3ee9 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.16.0" +__version__ = "0.17.0" From 0fbf49b4159186ee77e5bc93a762d57f8c67abb0 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 6 Oct 2020 17:31:54 -0400 Subject: [PATCH 471/649] Add py39 (#863) * Add py39 * Update dependencies --- .github/workflows/continuous_integration.yml | 2 +- .pre-commit-config.yaml | 4 ++-- Makefile | 1 + requirements.txt | 6 +++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index d800f399c..3cf3ee040 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3", "2.7", "3.5", "3.6", "3.7", "3.8", "3.9-dev"] + python-version: ["pypy3", "2.7", "3.5", "3.6", "3.7", "3.8", "3.9"] 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 1f5128595..cbbc42e80 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: check-merge-conflict - id: debug-statements - repo: https://github.com/timothycrosley/isort - rev: 5.4.2 + rev: 5.5.4 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade @@ -35,7 +35,7 @@ repos: - id: black args: [--safe, --quiet] - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.3 + rev: 3.8.4 hooks: - id: flake8 additional_dependencies: [flake8-bugbear] diff --git a/Makefile b/Makefile index f294985dc..41b351912 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ build39: PYTHON_VER = python3.9 build27 build35 build36 build37 build38 build39: clean virtualenv venv --python=$(PYTHON_VER) . venv/bin/activate; \ + pip install -U pip setuptools wheel; \ pip install -r requirements.txt; \ pre-commit install diff --git a/requirements.txt b/requirements.txt index df565d838..bf4415764 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ backports.functools_lru_cache==1.6.1; python_version == "2.7" dateparser==0.7.* pre-commit==1.21.*; python_version <= "3.5" -pre-commit==2.6.*; python_version >= "3.6" +pre-commit==2.7.*; python_version >= "3.6" pytest==4.6.*; python_version == "2.7" -pytest==6.0.*; python_version >= "3.5" +pytest==6.1.*; python_version >= "3.5" pytest-cov==2.10.* pytest-mock==2.0.*; python_version == "2.7" -pytest-mock==3.2.*; python_version >= "3.5" +pytest-mock==3.3.*; python_version >= "3.5" python-dateutil==2.8.* pytz==2019.* simplejson==3.17.* From 3709e32da3fc81d817a27e1ddb6bfe7143d294c9 Mon Sep 17 00:00:00 2001 From: Quentin Peter Date: Wed, 7 Oct 2020 18:17:14 +0200 Subject: [PATCH 472/649] Update locales.py (#865) Fixes: ``` (arrow.get() - timedelta(seconds=33)).humanize(locale="fr") 'il y a 33 quelques secondes' ``` Either say "a few seconds ago": "il y a quelques secondes" or "33 seconds ago": "il y a 33 secondes", not "a few 33 seconds ago" --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index 6833da5a7..cabe85d9d 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -487,7 +487,7 @@ class FrenchBaseLocale(Locale): timeframes = { "now": "maintenant", "second": "une seconde", - "seconds": "{0} quelques secondes", + "seconds": "{0} secondes", "minute": "une minute", "minutes": "{0} minutes", "hour": "une heure", From 7cccf6a7034bea1616d1df413f90bf902be77a57 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 13 Oct 2020 23:33:54 -0400 Subject: [PATCH 473/649] Raise ParserError rather than TypeError/re.error in certain scenarios. (#862) Co-authored-by: Chris <30196510+systemcatch@users.noreply.github.com> --- arrow/parser.py | 17 ++++++++++++++++- tests/test_parser.py | 21 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/arrow/parser.py b/arrow/parser.py index 243fd1721..50cdcf4d2 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -224,7 +224,13 @@ def parse(self, datetime_string, fmt, normalize_whitespace=False): if isinstance(fmt, list): return self._parse_multiformat(datetime_string, fmt) - fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt) + try: + fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt) + # TODO: remove pragma when we drop 2.7 + except re.error as e: # pragma: no cover + raise ParserMatchError( + "Failed to generate regular expression pattern: {}".format(e) + ) match = fmt_pattern_re.search(datetime_string) @@ -241,6 +247,15 @@ def parse(self, datetime_string, fmt, normalize_whitespace=False): value = (match.group("year"), match.group("week"), match.group("day")) else: value = match.group(token) + + # TODO: remove pragma when we drop 2.7 + if value is None: # pragma: no cover + raise ParserMatchError( + "Unable to find a match group for the specified token '{}'.".format( + token + ) + ) + self._parse_token(token, value, parts) return self._build_datetime(parts) diff --git a/tests/test_parser.py b/tests/test_parser.py index 9fb4e68f3..9e497f504 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -3,6 +3,7 @@ import calendar import os +import sys import time from datetime import datetime @@ -1655,3 +1656,23 @@ def test_escape(self): assert self.parser.parse( "Dec 31, 2017 |^${}().*+?<>-& 2:00 AM", format ) == datetime(2017, 12, 31, 2, 0) + + +@pytest.mark.skipif(sys.version_info < (3, 5), reason="requires python3.5 or higher") +@pytest.mark.usefixtures("dt_parser") +class TestFuzzInput: + # Regression test for issue #860 + def test_no_match_group(self): + fmt_str = str(b"[|\x1f\xb9\x03\x00\x00\x00\x00:-yI:][\x01yI:yI:I") + payload = str(b"") + + with pytest.raises(parser.ParserMatchError): + self.parser.parse(payload, fmt_str) + + # Regression test for issue #854 + def test_regex_module_error(self): + fmt_str = str(b"struct n[X+,N-M)MMXdMM]<") + payload = str(b"") + + with pytest.raises(parser.ParserMatchError): + self.parser.parse(payload, fmt_str) From 1b12fb2b57fe2e572cd326fdd1ee23215142cd3e Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 20 Oct 2020 00:01:26 -0400 Subject: [PATCH 474/649] Remove linting targets from tox envlist (#870) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 46576b12e..c8e1a3e45 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.18.0 -envlist = py{py3,27,35,36,37,38,39},lint,docs +envlist = py{py3,27,35,36,37,38,39} skip_missing_interpreters = true [gh-actions] From fe9602e2171d49d0d95d775e71f9735c3aaf92e4 Mon Sep 17 00:00:00 2001 From: kosuke316 <54512179+kosuke316@users.noreply.github.com> Date: Thu, 22 Oct 2020 19:17:53 +0900 Subject: [PATCH 475/649] Correct mistakes in Japanese locale (#873) * Correction of japanese mistakes. * Correction of japanese mistakes and implementation of test. * Removed /contributors --- arrow/locales.py | 5 +++-- tests/test_locales.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index cabe85d9d..c441fbd2c 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -656,11 +656,12 @@ class JapaneseLocale(Locale): past = "{0}前" future = "{0}後" + and_word = "" timeframes = { "now": "現在", - "second": "二番目の", - "seconds": "{0}数秒", + "second": "1秒", + "seconds": "{0}秒", "minute": "1分", "minutes": "{0}分", "hour": "1時間", diff --git a/tests/test_locales.py b/tests/test_locales.py index 006ccdd5b..76323f379 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1350,3 +1350,23 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(11) == "11번째" assert self.locale.ordinal_number(12) == "12번째" assert self.locale.ordinal_number(100) == "100번째" + + +@pytest.mark.usefixtures("lang_locale") +class TestJapaneseLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "現在" + assert self.locale._format_timeframe("second", 1) == "1秒" + assert self.locale._format_timeframe("seconds", 30) == "30秒" + assert self.locale._format_timeframe("minute", 1) == "1分" + assert self.locale._format_timeframe("minutes", 40) == "40分" + assert self.locale._format_timeframe("hour", 1) == "1時間" + assert self.locale._format_timeframe("hours", 23) == "23時間" + assert self.locale._format_timeframe("day", 1) == "1日" + assert self.locale._format_timeframe("days", 12) == "12日" + assert self.locale._format_timeframe("week", 1) == "1週間" + assert self.locale._format_timeframe("weeks", 38) == "38週間" + assert self.locale._format_timeframe("month", 1) == "1ヶ月" + assert self.locale._format_timeframe("months", 11) == "11ヶ月" + assert self.locale._format_timeframe("year", 1) == "1年" + assert self.locale._format_timeframe("years", 12) == "12年" From 609b630b9f5a4b814377053965b93d78fd979c64 Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Sun, 8 Nov 2020 21:38:24 +0000 Subject: [PATCH 476/649] Change humanize() granularity="auto" limits (#868) * Change humanize() granularity="auto" limits * Fix broken tests * Try different approach using class attributes * Change _SECONDS_PER_YEAR to non leap value * Get rid of separate constants and use attributes * Mark known test failure * Get coverage back to 100% * Minor changes Co-authored-by: Jad Chaar --- arrow/arrow.py | 39 +++++++++++++++++++-------------------- tests/test_arrow.py | 27 ++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 4fe954178..f3706b5b2 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -72,7 +72,7 @@ class Arrow(object): _SECS_PER_DAY = float(60 * 60 * 24) _SECS_PER_WEEK = float(60 * 60 * 24 * 7) _SECS_PER_MONTH = float(60 * 60 * 24 * 30.5) - _SECS_PER_YEAR = float(60 * 60 * 24 * 365.25) + _SECS_PER_YEAR = float(60 * 60 * 24 * 365) def __init__( self, @@ -1012,42 +1012,41 @@ def humanize( if diff < 10: return locale.describe("now", only_distance=only_distance) - if diff < 45: + if diff < self._SECS_PER_MINUTE: seconds = sign * delta return locale.describe( "seconds", seconds, only_distance=only_distance ) - elif diff < 90: + elif diff < self._SECS_PER_MINUTE * 2: return locale.describe("minute", sign, only_distance=only_distance) - elif diff < 2700: - minutes = sign * int(max(delta / 60, 2)) + elif diff < self._SECS_PER_HOUR: + minutes = sign * int(max(delta / self._SECS_PER_MINUTE, 2)) return locale.describe( "minutes", minutes, only_distance=only_distance ) - elif diff < 5400: + elif diff < self._SECS_PER_HOUR * 2: return locale.describe("hour", sign, only_distance=only_distance) - elif diff < 79200: - hours = sign * int(max(delta / 3600, 2)) + elif diff < self._SECS_PER_DAY: + hours = sign * int(max(delta / self._SECS_PER_HOUR, 2)) return locale.describe("hours", hours, only_distance=only_distance) - - # anything less than 48 hours should be 1 day - elif diff < 172800: + elif diff < self._SECS_PER_DAY * 2: return locale.describe("day", sign, only_distance=only_distance) - elif diff < 554400: - days = sign * int(max(delta / 86400, 2)) + elif diff < self._SECS_PER_WEEK: + days = sign * int(max(delta / self._SECS_PER_DAY, 2)) return locale.describe("days", days, only_distance=only_distance) - elif diff < 907200: + elif diff < self._SECS_PER_WEEK * 2: return locale.describe("week", sign, only_distance=only_distance) - elif diff < 2419200: - weeks = sign * int(max(delta / 604800, 2)) + elif diff < self._SECS_PER_MONTH: + weeks = sign * int(max(delta / self._SECS_PER_WEEK, 2)) return locale.describe("weeks", weeks, only_distance=only_distance) - elif diff < 3888000: + elif diff < self._SECS_PER_MONTH * 2: return locale.describe("month", sign, only_distance=only_distance) - elif diff < 29808000: + 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 @@ -1057,10 +1056,10 @@ def humanize( "months", months, only_distance=only_distance ) - elif diff < 47260800: + elif diff < self._SECS_PER_YEAR * 2: return locale.describe("year", sign, only_distance=only_distance) else: - years = sign * int(max(delta / 31536000, 2)) + years = sign * int(max(delta / self._SECS_PER_YEAR, 2)) return locale.describe("years", years, only_distance=only_distance) elif util.isstr(granularity): diff --git a/tests/test_arrow.py b/tests/test_arrow.py index b0bd20a5e..872edc017 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -1772,11 +1772,12 @@ def test_multiple_granularity(self): ) == "37 months and 4 weeks" ) + # this will change when leap years are implemented assert ( self.now.humanize( later108onlydistance, only_distance=True, granularity=["year", "second"] ) - == "3 years and 5327200 seconds" + == "3 years and 5392000 seconds" ) one_min_one_sec_ago = self.now.shift(minutes=-1, seconds=-1) @@ -1909,16 +1910,26 @@ 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" + def test_months(self): later = self.now.shift(months=2) @@ -1954,7 +1965,7 @@ def test_years(self): result = arw.humanize(self.datetime) - assert result == "in 2 years" + assert result == "in a year" def test_arrow(self): @@ -1998,6 +2009,16 @@ def test_none(self): assert result == "just now" + def test_week_limit(self): + # regression test for issue #848 + arw = arrow.Arrow.utcnow() + + later = arw.shift(weeks=+1) + + result = arw.humanize(later) + + assert result == "a week ago" + def test_untranslated_granularity(self, mocker): arw = arrow.Arrow.utcnow() @@ -2033,7 +2054,7 @@ def test_years(self): result = arw.humanize(self.datetime, locale="ru") - assert result == "2 года назад" + assert result == "год назад" class TestArrowIsBetween: From 30d16c1cff0d6cc5b50b5fa5b19a9455f54f99fc Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Tue, 10 Nov 2020 16:02:54 -0500 Subject: [PATCH 477/649] Deprecate Python 2.7 and Python 3.5 (#877) * Begin new branch for py27/35 changes. Drop 27/35 from CI. * Run pre-commit hooks * Deprecate timestamp property and fix tests * More removals for 36+ * More timestamp updates and cleaned up Makefile --- .github/workflows/continuous_integration.yml | 2 +- .pre-commit-config.yaml | 13 ++-- Makefile | 21 +++--- README.rst | 2 +- arrow/__init__.py | 1 - arrow/api.py | 2 - arrow/arrow.py | 70 +++++--------------- arrow/constants.py | 15 +++-- arrow/factory.py | 20 +++--- arrow/formatter.py | 29 +++----- arrow/locales.py | 65 +++++++++--------- arrow/parser.py | 45 +++++-------- arrow/util.py | 29 +------- docs/conf.py | 7 +- requirements.txt | 15 ++--- setup.cfg | 2 - setup.py | 20 ++---- tests/conftest.py | 1 - tests/test_api.py | 1 - tests/test_arrow.py | 28 ++------ tests/test_factory.py | 7 +- tests/test_formatter.py | 8 +-- tests/test_locales.py | 3 - tests/test_parser.py | 36 +++------- tests/test_util.py | 1 - tests/utils.py | 1 - tox.ini | 4 +- 27 files changed, 155 insertions(+), 293 deletions(-) delete mode 100644 setup.cfg diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 3cf3ee040..350bbb4f5 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3", "2.7", "3.5", "3.6", "3.7", "3.8", "3.9"] + python-version: ["pypy3", "3.6", "3.7", "3.8", "3.9"] 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 cbbc42e80..d456b221b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,12 +2,12 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v3.3.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: fix-encoding-pragma - exclude: ^arrow/_version.py + args: [--remove] - id: requirements-txt-fixer - id: check-ast - id: check-yaml @@ -16,15 +16,16 @@ repos: - id: check-merge-conflict - id: debug-statements - repo: https://github.com/timothycrosley/isort - rev: 5.5.4 + rev: 5.6.4 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.7.2 + rev: v2.7.3 hooks: - id: pyupgrade + args: [--py36-plus] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.6.0 + rev: v1.7.0 hooks: - id: python-no-eval - id: python-check-blanket-noqa @@ -33,7 +34,7 @@ repos: rev: 20.8b1 hooks: - id: black - args: [--safe, --quiet] + args: [--safe, --quiet, --target-version=py36] - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.4 hooks: diff --git a/Makefile b/Makefile index 41b351912..18a7fbc5e 100644 --- a/Makefile +++ b/Makefile @@ -2,15 +2,13 @@ auto: build38 -build27: PYTHON_VER = python2.7 -build35: PYTHON_VER = python3.5 build36: PYTHON_VER = python3.6 build37: PYTHON_VER = python3.7 build38: PYTHON_VER = python3.8 build39: PYTHON_VER = python3.9 -build27 build35 build36 build37 build38 build39: clean - virtualenv venv --python=$(PYTHON_VER) +build36 build37 build38 build39: clean + $(PYTHON_VER) -m venv venv . venv/bin/activate; \ pip install -U pip setuptools wheel; \ pip install -r requirements.txt; \ @@ -18,14 +16,18 @@ build27 build35 build36 build37 build38 build39: clean test: rm -f .coverage coverage.xml - . venv/bin/activate; pytest + . venv/bin/activate; \ + pytest lint: - . venv/bin/activate; pre-commit run --all-files --show-diff-on-failure + . venv/bin/activate; \ + pre-commit run --all-files --show-diff-on-failure docs: rm -rf docs/_build - . venv/bin/activate; cd docs; make html + . venv/bin/activate; \ + cd docs; \ + make html clean: clean-dist rm -rf venv .pytest_cache ./**/__pycache__ @@ -36,10 +38,11 @@ clean-dist: build-dist: . venv/bin/activate; \ - pip install -U setuptools twine wheel; \ + pip install -U pip setuptools twine wheel; \ python setup.py sdist bdist_wheel upload-dist: - . venv/bin/activate; twine upload dist/* + . venv/bin/activate; \ + twine upload dist/* publish: test clean-dist build-dist upload-dist clean-dist diff --git a/README.rst b/README.rst index 69f6c50d8..3571c71e4 100644 --- a/README.rst +++ b/README.rst @@ -47,7 +47,7 @@ Features -------- - Fully-implemented, drop-in replacement for datetime -- Supports Python 2.7, 3.5, 3.6, 3.7, 3.8 and 3.9 +- Supports Python 3.6+ - Timezone-aware and UTC by default - Provides super-simple creation options for many common input scenarios - :code:`shift` method with support for relative offsets, including weeks diff --git a/arrow/__init__.py b/arrow/__init__.py index 2883527be..117c9e8a0 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from ._version import __version__ from .api import get, now, utcnow from .arrow import Arrow diff --git a/arrow/api.py b/arrow/api.py index a6b7be3de..13a369f82 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 -*- """ Provides the default implementation of :class:`ArrowFactory ` methods for use as a module API. """ -from __future__ import absolute_import from arrow.factory import ArrowFactory diff --git a/arrow/arrow.py b/arrow/arrow.py index f3706b5b2..899c680c9 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1,15 +1,12 @@ -# -*- coding: utf-8 -*- """ Provides the :class:`Arrow ` class, an enhanced ``datetime`` replacement. """ -from __future__ import absolute_import import calendar import sys -import warnings from datetime import datetime, timedelta from datetime import tzinfo as dt_tzinfo from math import trunc @@ -19,17 +16,8 @@ from arrow import formatter, locales, parser, util -if sys.version_info[:2] < (3, 6): # pragma: no cover - with warnings.catch_warnings(): - warnings.simplefilter("default", DeprecationWarning) - warnings.warn( - "Arrow will drop support for Python 2.7 and 3.5 in the upcoming v1.0.0 release. Please upgrade to " - "Python 3.6+ to continue receiving updates for Arrow.", - DeprecationWarning, - ) - -class Arrow(object): +class Arrow: """An :class:`Arrow ` object. Implements the ``datetime`` interface, behaving as an aware ``datetime`` while implementing @@ -65,7 +53,7 @@ class Arrow(object): resolution = datetime.resolution _ATTRS = ["year", "month", "day", "hour", "minute", "second", "microsecond"] - _ATTRS_PLURAL = ["{}s".format(a) for a in _ATTRS] + _ATTRS_PLURAL = [f"{a}s" for a in _ATTRS] _MONTHS_PER_QUARTER = 3 _SECS_PER_MINUTE = float(60) _SECS_PER_HOUR = float(60 * 60) @@ -84,7 +72,7 @@ def __init__( second=0, microsecond=0, tzinfo=None, - **kwargs + **kwargs, ): if tzinfo is None: tzinfo = dateutil_tz.tzutc() @@ -96,7 +84,7 @@ def __init__( and tzinfo.zone ): tzinfo = parser.TzinfoParser.parse(tzinfo.zone) - elif util.isstr(tzinfo): + elif isinstance(tzinfo, str): tzinfo = parser.TzinfoParser.parse(tzinfo) fold = kwargs.get("fold", 0) @@ -177,13 +165,11 @@ def fromtimestamp(cls, timestamp, tzinfo=None): if tzinfo is None: tzinfo = dateutil_tz.tzlocal() - elif util.isstr(tzinfo): + elif isinstance(tzinfo, str): tzinfo = parser.TzinfoParser.parse(tzinfo) if not util.is_timestamp(timestamp): - raise ValueError( - "The provided timestamp '{}' is invalid.".format(timestamp) - ) + raise ValueError(f"The provided timestamp '{timestamp}' is invalid.") timestamp = util.normalize_timestamp(float(timestamp)) dt = datetime.fromtimestamp(timestamp, tzinfo) @@ -209,9 +195,7 @@ def utcfromtimestamp(cls, timestamp): """ if not util.is_timestamp(timestamp): - raise ValueError( - "The provided timestamp '{}' is invalid.".format(timestamp) - ) + raise ValueError(f"The provided timestamp '{timestamp}' is invalid.") timestamp = util.normalize_timestamp(float(timestamp)) dt = datetime.utcfromtimestamp(timestamp) @@ -604,7 +588,7 @@ def interval(cls, frame, start, end, interval=1, tz=None, bounds="[)"): # representations def __repr__(self): - return "<{} [{}]>".format(self.__class__.__name__, self.__str__()) + return f"<{self.__class__.__name__} [{self.__str__()}]>" def __str__(self): return self._datetime.isoformat() @@ -688,7 +672,6 @@ def naive(self): return self._datetime.replace(tzinfo=None) - @property def timestamp(self): """Returns a timestamp representation of the :class:`Arrow ` object, in UTC time. @@ -700,13 +683,7 @@ def timestamp(self): """ - warnings.warn( - "For compatibility with the datetime.timestamp() method this property will be replaced with a method in " - "the 1.0.0 release, please switch to the .int_timestamp property for identical behaviour as soon as " - "possible.", - DeprecationWarning, - ) - return calendar.timegm(self._datetime.utctimetuple()) + return self._datetime.timestamp() @property def int_timestamp(self): @@ -720,7 +697,7 @@ def int_timestamp(self): """ - return calendar.timegm(self._datetime.utctimetuple()) + return int(self.timestamp()) @property def float_timestamp(self): @@ -734,11 +711,7 @@ def float_timestamp(self): """ - # IDEA get rid of this in 1.0.0 and wrap datetime.timestamp() - # Or for compatibility retain this but make it call the timestamp method - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - return self.timestamp + float(self.microsecond) / 1000000 + return self.timestamp() @property def fold(self): @@ -802,9 +775,9 @@ def replace(self, **kwargs): if key in self._ATTRS: absolute_kwargs[key] = value elif key in ["week", "quarter"]: - raise AttributeError("setting absolute {} is not supported".format(key)) + raise AttributeError(f"setting absolute {key} is not supported") elif key not in ["tzinfo", "fold"]: - raise AttributeError('unknown attribute: "{}"'.format(key)) + raise AttributeError(f'unknown attribute: "{key}"') current = self._datetime.replace(**absolute_kwargs) @@ -1062,7 +1035,7 @@ def humanize( years = sign * int(max(delta / self._SECS_PER_YEAR, 2)) return locale.describe("years", years, only_distance=only_distance) - elif util.isstr(granularity): + elif isinstance(granularity, str): if granularity == "second": delta = sign * delta if abs(delta) < 2: @@ -1491,13 +1464,6 @@ def __le__(self, other): return self._datetime <= self._get_datetime(other) - def __cmp__(self, other): - if sys.version_info[0] < 3: # pragma: no cover - if not isinstance(other, (Arrow, datetime)): - raise TypeError( - "can't compare '{}' to '{}'".format(type(self), type(other)) - ) - # internal methods @staticmethod @@ -1511,7 +1477,7 @@ def _get_tzinfo(tz_expr): try: return parser.TzinfoParser.parse(tz_expr) except parser.ParserError: - raise ValueError("'{}' not recognized as a timezone".format(tz_expr)) + raise ValueError(f"'{tz_expr}' not recognized as a timezone") @classmethod def _get_datetime(cls, expr): @@ -1524,15 +1490,13 @@ def _get_datetime(cls, expr): timestamp = float(expr) return cls.utcfromtimestamp(timestamp).datetime else: - raise ValueError( - "'{}' not recognized as a datetime or timestamp.".format(expr) - ) + raise ValueError(f"'{expr}' not recognized as a datetime or timestamp.") @classmethod def _get_frames(cls, name): if name in cls._ATTRS: - return name, "{}s".format(name), 1 + return name, f"{name}s", 1 elif name[-1] == "s" and name[:-1] in cls._ATTRS: return name[:-1], name, 1 elif name in ["week", "weeks"]: diff --git a/arrow/constants.py b/arrow/constants.py index 81e37b26d..d63698448 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -1,9 +1,12 @@ -# -*- coding: utf-8 -*- +import os +from datetime import datetime -# Output of time.mktime(datetime.max.timetuple()) on macOS -# This value must be hardcoded for compatibility with Windows -# Platform-independent max timestamps are hard to form -# https://stackoverflow.com/q/46133223 -MAX_TIMESTAMP = 253402318799.0 +# datetime.max.timestamp() errors on Windows, so we must hardcode +# the highest possible datetime value that can output a timestamp. +# tl;dr platform-independent max timestamps are hard to form +# See: https://stackoverflow.com/q/46133223 +MAX_TIMESTAMP = ( + datetime(3001, 1, 18, 23, 59, 59, 999999) if os.name == "nt" else datetime.max +).timestamp() MAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1000 MAX_TIMESTAMP_US = MAX_TIMESTAMP * 1000000 diff --git a/arrow/factory.py b/arrow/factory.py index 05933e815..9bae80c58 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Implements the :class:`ArrowFactory ` class, providing factory methods for common :class:`Arrow ` @@ -6,7 +5,6 @@ """ -from __future__ import absolute_import import calendar from datetime import date, datetime @@ -17,10 +15,10 @@ from arrow import parser from arrow.arrow import Arrow -from arrow.util import is_timestamp, iso_to_gregorian, isstr +from arrow.util import is_timestamp, iso_to_gregorian -class ArrowFactory(object): +class ArrowFactory: """A factory for generating :class:`Arrow ` objects. :param type: (optional) the :class:`Arrow `-based class to construct from. @@ -155,7 +153,7 @@ def get(self, *args, **kwargs): # () -> now, @ utc. if arg_count == 0: - if isstr(tz): + if isinstance(tz, str): tz = parser.TzinfoParser.parse(tz) return self.type.now(tz) @@ -172,7 +170,7 @@ def get(self, *args, **kwargs): return self.type.utcnow() # try (int, float) -> from timestamp with tz - elif not isstr(arg) and is_timestamp(arg): + elif not isinstance(arg, str) and is_timestamp(arg): if tz is None: # set to UTC by default tz = dateutil_tz.tzutc() @@ -195,7 +193,7 @@ def get(self, *args, **kwargs): return self.type.now(arg) # (str) -> parse. - elif isstr(arg): + elif isinstance(arg, str): dt = parser.DateTimeParser(locale).parse_iso(arg, normalize_whitespace) return self.type.fromdatetime(dt, tz) @@ -220,7 +218,7 @@ def get(self, *args, **kwargs): if isinstance(arg_1, datetime): # (datetime, tzinfo/str) -> fromdatetime replace tzinfo. - if isinstance(arg_2, dt_tzinfo) or isstr(arg_2): + if isinstance(arg_2, dt_tzinfo) or isinstance(arg_2, str): return self.type.fromdatetime(arg_1, arg_2) else: raise TypeError( @@ -232,7 +230,7 @@ def get(self, *args, **kwargs): elif isinstance(arg_1, date): # (date, tzinfo/str) -> fromdate replace tzinfo. - if isinstance(arg_2, dt_tzinfo) or isstr(arg_2): + if isinstance(arg_2, dt_tzinfo) or isinstance(arg_2, str): return self.type.fromdate(arg_1, tzinfo=arg_2) else: raise TypeError( @@ -242,7 +240,9 @@ def get(self, *args, **kwargs): ) # (str, format) -> parse. - elif isstr(arg_1) and (isstr(arg_2) or isinstance(arg_2, list)): + elif isinstance(arg_1, str) and ( + isinstance(arg_2, str) or isinstance(arg_2, list) + ): dt = parser.DateTimeParser(locale).parse( args[0], args[1], normalize_whitespace ) diff --git a/arrow/formatter.py b/arrow/formatter.py index 9f9d7a44d..6bd61c89d 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division - -import calendar import re from dateutil import tz as dateutil_tz @@ -20,7 +16,7 @@ FORMAT_W3C = "YYYY-MM-DD HH:mm:ssZZ" -class DateTimeFormatter(object): +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 @@ -53,16 +49,16 @@ def _format_token(self, dt, token): if token == "MMM": return self.locale.month_abbreviation(dt.month) if token == "MM": - return "{:02d}".format(dt.month) + return f"{dt.month:02d}" if token == "M": return str(dt.month) if token == "DDDD": - return "{:03d}".format(dt.timetuple().tm_yday) + return f"{dt.timetuple().tm_yday:03d}" if token == "DDD": return str(dt.timetuple().tm_yday) if token == "DD": - return "{:02d}".format(dt.day) + return f"{dt.day:02d}" if token == "D": return str(dt.day) @@ -77,7 +73,7 @@ def _format_token(self, dt, token): return str(dt.isoweekday()) if token == "HH": - return "{:02d}".format(dt.hour) + return f"{dt.hour:02d}" if token == "H": return str(dt.hour) if token == "hh": @@ -86,12 +82,12 @@ def _format_token(self, dt, token): return str(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) if token == "mm": - return "{:02d}".format(dt.minute) + return f"{dt.minute:02d}" if token == "m": return str(dt.minute) if token == "ss": - return "{:02d}".format(dt.second) + return f"{dt.second:02d}" if token == "s": return str(dt.second) @@ -109,13 +105,10 @@ def _format_token(self, dt, token): return str(int(dt.microsecond / 100000)) if token == "X": - # TODO: replace with a call to dt.timestamp() when we drop Python 2.7 - return str(calendar.timegm(dt.utctimetuple())) + return str(dt.timestamp()) if token == "x": - # TODO: replace with a call to dt.timestamp() when we drop Python 2.7 - ts = calendar.timegm(dt.utctimetuple()) + (dt.microsecond / 1000000) - return str(int(ts * 1000000)) + return str(int(dt.timestamp() * 1000000)) if token == "ZZZ": return dt.tzname() @@ -129,11 +122,11 @@ def _format_token(self, dt, token): total_minutes = abs(total_minutes) hour, minute = divmod(total_minutes, 60) - return "{}{:02d}{}{:02d}".format(sign, hour, separator, minute) + return f"{sign}{hour:02d}{separator}{minute:02d}" if token in ("a", "A"): return self.locale.meridian(dt.hour, token) if token == "W": year, week, day = dt.isocalendar() - return "{}-W{:02d}-{}".format(year, week, day) + return f"{year}-W{week:02d}-{day}" diff --git a/arrow/locales.py b/arrow/locales.py index c441fbd2c..3d0ca4637 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - import inspect import sys from math import trunc @@ -17,7 +14,7 @@ def get_locale(name): locale_cls = _locales.get(name.lower()) if locale_cls is None: - raise ValueError("Unsupported locale '{}'".format(name)) + raise ValueError(f"Unsupported locale '{name}'") return locale_cls() @@ -32,7 +29,7 @@ def get_locale_by_class_name(name): locale_cls = globals().get(name) if locale_cls is None: - raise ValueError("Unsupported locale '{}'".format(name)) + raise ValueError(f"Unsupported locale '{name}'") return locale_cls() @@ -40,7 +37,7 @@ def get_locale_by_class_name(name): # base locale type. -class Locale(object): +class Locale: """ Represents locale-specific data and functionality. """ names = [] @@ -171,14 +168,14 @@ def year_full(self, year): :param name: the ``int`` year (4-digit) """ - return "{:04d}".format(year) + return f"{year:04d}" def year_abbreviation(self, year): """Returns the year for specific locale if available :param name: the ``int`` year (4-digit) """ - return "{:04d}".format(year)[2:] + return f"{year:04d}"[2:] def meridian(self, hour, token): """Returns the meridian indicator for a specified hour and format token. @@ -200,7 +197,7 @@ def ordinal_number(self, n): return self._ordinal_number(n) def _ordinal_number(self, n): - return "{}".format(n) + return f"{n}" def _name_to_ordinal(self, lst): return dict(map(lambda i: (i[1].lower(), i[0] + 1), enumerate(lst[1:]))) @@ -308,12 +305,12 @@ def _ordinal_number(self, n): if n % 100 not in (11, 12, 13): remainder = abs(n) % 10 if remainder == 1: - return "{}st".format(n) + return f"{n}st" elif remainder == 2: - return "{}nd".format(n) + return f"{n}nd" elif remainder == 3: - return "{}rd".format(n) - return "{}th".format(n) + return f"{n}rd" + return f"{n}th" def describe(self, timeframe, delta=0, only_distance=False): """Describes a delta within a timeframe in plain language. @@ -323,7 +320,7 @@ def describe(self, timeframe, delta=0, only_distance=False): :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords """ - humanized = super(EnglishLocale, self).describe(timeframe, delta, only_distance) + humanized = super().describe(timeframe, delta, only_distance) if only_distance and timeframe == "now": humanized = "instantly" @@ -400,7 +397,7 @@ class ItalianLocale(Locale): ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])" def _ordinal_number(self, n): - return "{}º".format(n) + return f"{n}º" class SpanishLocale(Locale): @@ -475,7 +472,7 @@ class SpanishLocale(Locale): ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])" def _ordinal_number(self, n): - return "{}º".format(n) + return f"{n}º" class FrenchBaseLocale(Locale): @@ -536,8 +533,8 @@ class FrenchBaseLocale(Locale): def _ordinal_number(self, n): if abs(n) == 1: - return "{}er".format(n) - return "{}e".format(n) + return f"{n}er" + return f"{n}e" class FrenchLocale(FrenchBaseLocale, Locale): @@ -869,7 +866,7 @@ def _format_relative(self, humanized, timeframe, delta): return direction.format(humanized[which]) def _ordinal_number(self, n): - return "{}.".format(n) + return f"{n}." class ChineseCNLocale(Locale): @@ -1128,7 +1125,7 @@ def _ordinal_number(self, n): ordinals = ["0", "첫", "두", "세", "네", "다섯", "여섯", "일곱", "여덟", "아홉", "열"] if n < len(ordinals): return "{}번째".format(ordinals[n]) - return "{}번째".format(n) + return f"{n}번째" def _format_relative(self, humanized, timeframe, delta): if timeframe in ("day", "days"): @@ -1140,7 +1137,7 @@ def _format_relative(self, humanized, timeframe, delta): if special: return special - return super(KoreanLocale, self)._format_relative(humanized, timeframe, delta) + return super()._format_relative(humanized, timeframe, delta) # derived locale types & implementations. @@ -1798,7 +1795,7 @@ class GermanBaseLocale(Locale): day_abbreviations = ["", "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] def _ordinal_number(self, n): - return "{}.".format(n) + return f"{n}." def describe(self, timeframe, delta=0, only_distance=False): """Describes a delta within a timeframe in plain language. @@ -1809,9 +1806,7 @@ def describe(self, timeframe, delta=0, only_distance=False): """ if not only_distance: - return super(GermanBaseLocale, self).describe( - timeframe, delta, only_distance - ) + 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))) @@ -2130,7 +2125,7 @@ class TagalogLocale(Locale): meridians = {"am": "nu", "pm": "nh", "AM": "ng umaga", "PM": "ng hapon"} def _ordinal_number(self, n): - return "ika-{}".format(n) + return f"ika-{n}" class VietnameseLocale(Locale): @@ -3186,7 +3181,7 @@ class HebrewLocale(Locale): def _format_timeframe(self, timeframe, delta): """Hebrew couple of aware""" - couple = "2-{}".format(timeframe) + couple = f"2-{timeframe}" single = timeframe.rstrip("s") if abs(delta) == 2 and couple in self.timeframes: key = couple @@ -3592,7 +3587,7 @@ class EsperantoLocale(Locale): ordinal_day_re = r"((?P[1-3]?[0-9](?=a))a)" def _ordinal_number(self, n): - return "{}a".format(n) + return f"{n}a" class ThaiLocale(Locale): @@ -3659,12 +3654,12 @@ class ThaiLocale(Locale): def year_full(self, year): """Thai always use Buddhist Era (BE) which is CE + 543""" year += self.BE_OFFSET - return "{:04d}".format(year) + return f"{year:04d}" def year_abbreviation(self, year): """Thai always use Buddhist Era (BE) which is CE + 543""" year += self.BE_OFFSET - return "{:04d}".format(year)[2:] + return f"{year:04d}"[2:] def _format_relative(self, humanized, timeframe, delta): """Thai normally doesn't have any space between words""" @@ -3746,15 +3741,15 @@ class BengaliLocale(Locale): def _ordinal_number(self, n): if n > 10 or n == 0: - return "{}তম".format(n) + return f"{n}তম" if n in [1, 5, 7, 8, 9, 10]: - return "{}ম".format(n) + return f"{n}ম" if n in [2, 3]: - return "{}য়".format(n) + return f"{n}য়" if n == 4: - return "{}র্থ".format(n) + return f"{n}র্থ" if n == 6: - return "{}ষ্ঠ".format(n) + return f"{n}ষ্ঠ" class RomanshLocale(Locale): diff --git a/arrow/parser.py b/arrow/parser.py index 50cdcf4d2..21ff3335a 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -1,19 +1,12 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - import re from datetime import datetime, timedelta +from functools import lru_cache from dateutil import tz from arrow import locales from arrow.util import iso_to_gregorian, next_weekday, normalize_timestamp -try: - from functools import lru_cache -except ImportError: # pragma: no cover - from backports.functools_lru_cache import lru_cache # pragma: no cover - class ParserError(ValueError): pass @@ -28,7 +21,7 @@ class ParserMatchError(ParserError): pass -class DateTimeParser(object): +class DateTimeParser: _FORMAT_RE = 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)" @@ -200,19 +193,19 @@ def parse_iso(self, datetime_string, normalize_whitespace=False): elif has_seconds: time_string = "HH{time_sep}mm{time_sep}ss".format(time_sep=time_sep) elif has_minutes: - time_string = "HH{time_sep}mm".format(time_sep=time_sep) + time_string = f"HH{time_sep}mm" else: time_string = "HH" if has_space_divider: - formats = ["{} {}".format(f, time_string) for f in formats] + formats = [f"{f} {time_string}" for f in formats] else: - formats = ["{}T{}".format(f, time_string) for f in formats] + formats = [f"{f}T{time_string}" for f in formats] if has_time and has_tz: # Add "Z" or "ZZ" to the format strings to indicate to # _parse_token() that a timezone needs to be parsed - formats = ["{}{}".format(f, tz_format) for f in formats] + formats = [f"{f}{tz_format}" for f in formats] return self._parse_multiformat(datetime_string, formats) @@ -226,17 +219,16 @@ def parse(self, datetime_string, fmt, normalize_whitespace=False): try: fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt) - # TODO: remove pragma when we drop 2.7 - except re.error as e: # pragma: no cover + except re.error as e: raise ParserMatchError( - "Failed to generate regular expression pattern: {}".format(e) + f"Failed to generate regular expression pattern: {e}" ) match = fmt_pattern_re.search(datetime_string) if match is None: raise ParserMatchError( - "Failed to match '{}' when parsing '{}'".format(fmt, datetime_string) + f"Failed to match '{fmt}' when parsing '{datetime_string}'" ) parts = {} @@ -248,8 +240,7 @@ def parse(self, datetime_string, fmt, normalize_whitespace=False): else: value = match.group(token) - # TODO: remove pragma when we drop 2.7 - if value is None: # pragma: no cover + if value is None: raise ParserMatchError( "Unable to find a match group for the specified token '{}'.".format( token @@ -288,8 +279,8 @@ def _generate_pattern_re(self, fmt): try: input_re = self._input_re_map[token] except KeyError: - raise ParserError("Unrecognized token '{}'".format(token)) - input_pattern = "(?P<{}>{})".format(token, input_re.pattern) + raise ParserError(f"Unrecognized token '{token}'") + input_pattern = f"(?P<{token}>{input_re.pattern})" tokens.append(token) # a pattern doesn't have the same length as the token # it replaces! We keep the difference in the offset variable. @@ -389,7 +380,7 @@ def _parse_token(self, token, value, parts): # We have the *most significant* digits of an arbitrary-precision integer. # We want the six most significant digits as an integer, rounded. # IDEA: add nanosecond support somehow? Need datetime support for it first. - value = value.ljust(7, str("0")) + value = value.ljust(7, "0") # floating-point (IEEE-754) defaults to half-to-even rounding seventh_digit = int(value[6]) @@ -468,12 +459,12 @@ def _build_datetime(parts): "Month component is not allowed with the DDD and DDDD tokens." ) - date_string = "{}-{}".format(year, day_of_year) + date_string = f"{year}-{day_of_year}" try: dt = datetime.strptime(date_string, "%Y-%j") except ValueError: raise ParserError( - "The provided day of year '{}' is invalid.".format(day_of_year) + f"The provided day of year '{day_of_year}' is invalid." ) parts["year"] = dt.year @@ -571,7 +562,7 @@ def _generate_choice_re(choices, flags=0): return re.compile(r"({})".format("|".join(choices)), flags=flags) -class TzinfoParser(object): +class TzinfoParser: _TZINFO_RE = re.compile(r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$") @classmethod @@ -604,8 +595,6 @@ def parse(cls, tzinfo_string): tzinfo = tz.gettz(tzinfo_string) if tzinfo is None: - raise ParserError( - 'Could not parse timezone expression "{}"'.format(tzinfo_string) - ) + raise ParserError(f'Could not parse timezone expression "{tzinfo_string}"') return tzinfo diff --git a/arrow/util.py b/arrow/util.py index acce8878d..7053a8863 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -1,8 +1,4 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - import datetime -import numbers from dateutil.rrule import WEEKLY, rrule @@ -45,9 +41,7 @@ def is_timestamp(value): if isinstance(value, bool): return False if not ( - isinstance(value, numbers.Integral) - or isinstance(value, float) - or isinstance(value, str) + isinstance(value, int) or isinstance(value, float) or isinstance(value, str) ): return False try: @@ -65,9 +59,7 @@ def normalize_timestamp(timestamp): elif timestamp < MAX_TIMESTAMP_US: timestamp /= 1e6 else: - raise ValueError( - "The specified timestamp '{}' is too large.".format(timestamp) - ) + raise ValueError(f"The specified timestamp '{timestamp}' is too large.") return timestamp @@ -97,19 +89,4 @@ def validate_bounds(bounds): ) -# Python 2.7 / 3.0+ definitions for isstr function. - -try: # pragma: no cover - basestring - - def isstr(s): - return isinstance(s, basestring) # noqa: F821 - - -except NameError: # pragma: no cover - - def isstr(s): - return isinstance(s, str) - - -__all__ = ["next_weekday", "total_seconds", "is_timestamp", "isstr", "iso_to_gregorian"] +__all__ = ["next_weekday", "total_seconds", "is_timestamp", "iso_to_gregorian"] diff --git a/docs/conf.py b/docs/conf.py index aaf3c5082..ce16c080e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,20 +1,17 @@ -# -*- coding: utf-8 -*- - # -- Path setup -------------------------------------------------------------- -import io import os import sys sys.path.insert(0, os.path.abspath("..")) about = {} -with io.open("../arrow/_version.py", "r", encoding="utf-8") as f: +with open("../arrow/_version.py", encoding="utf-8") as f: exec(f.read(), about) # -- Project information ----------------------------------------------------- -project = u"Arrow 🏹" +project = "Arrow 🏹" copyright = "2020, Chris Smith" author = "Chris Smith" diff --git a/requirements.txt b/requirements.txt index bf4415764..bcf86d346 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,9 @@ -backports.functools_lru_cache==1.6.1; python_version == "2.7" -dateparser==0.7.* -pre-commit==1.21.*; python_version <= "3.5" -pre-commit==2.7.*; python_version >= "3.6" -pytest==4.6.*; python_version == "2.7" -pytest==6.1.*; python_version >= "3.5" +dateparser==1.0.* +pre-commit==2.8.* +pytest==6.1.* pytest-cov==2.10.* -pytest-mock==2.0.*; python_version == "2.7" -pytest-mock==3.3.*; python_version >= "3.5" +pytest-mock==3.3.* python-dateutil==2.8.* pytz==2019.* simplejson==3.17.* -sphinx==1.8.*; python_version == "2.7" -sphinx==3.2.*; python_version >= "3.5" +sphinx==3.3.* diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2a9acf13d..000000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py index dc4f0e77d..703f1c548 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,10 @@ -# -*- coding: utf-8 -*- -import io - from setuptools import setup -with io.open("README.rst", "r", encoding="utf-8") as f: +with open("README.rst", encoding="utf-8") as f: readme = f.read() about = {} -with io.open("arrow/_version.py", "r", encoding="utf-8") as f: +with open("arrow/_version.py", encoding="utf-8") as f: exec(f.read(), about) setup( @@ -22,20 +19,15 @@ license="Apache 2.0", packages=["arrow"], zip_safe=False, - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", - install_requires=[ - "python-dateutil>=2.7.0", - "backports.functools_lru_cache>=1.2.1;python_version=='2.7'", - ], + python_requires=">=3.6", + install_requires=["python-dateutil>=2.7.0"], classifiers=[ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Topic :: Software Development :: Libraries :: Python Modules", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", diff --git a/tests/conftest.py b/tests/conftest.py index 5bc8a4af2..4043bc3b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from datetime import datetime import pytest diff --git a/tests/test_api.py b/tests/test_api.py index 9b19a27cd..5576aaf84 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import arrow diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 872edc017..fe583d98b 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -import calendar import pickle import sys import time @@ -193,7 +189,7 @@ def test_repr(self): result = self.arrow.__repr__() - assert result == "".format(self.arrow._datetime.isoformat()) + assert result == f"" def test_str(self): @@ -209,7 +205,7 @@ def test_hash(self): def test_format(self): - result = "{:YYYY-MM-DD}".format(self.arrow) + result = f"{self.arrow:YYYY-MM-DD}" assert result == "2013-02-03" @@ -221,7 +217,7 @@ def test_bare_format(self): def test_format_no_format_string(self): - result = "{}".format(self.arrow) + result = f"{self.arrow}" assert result == str(self.arrow) @@ -280,24 +276,15 @@ def test_naive(self): def test_timestamp(self): - assert self.arrow.timestamp == calendar.timegm( - self.arrow._datetime.utctimetuple() - ) - - with pytest.warns(DeprecationWarning): - self.arrow.timestamp + assert self.arrow.timestamp() == self.arrow._datetime.timestamp() def test_int_timestamp(self): - assert self.arrow.int_timestamp == calendar.timegm( - self.arrow._datetime.utctimetuple() - ) + assert self.arrow.int_timestamp == int(self.arrow._datetime.timestamp()) def test_float_timestamp(self): - result = self.arrow.float_timestamp - self.arrow.timestamp - - assert result == self.arrow.microsecond + assert self.arrow.float_timestamp == self.arrow._datetime.timestamp() def test_getattr_fold(self): @@ -922,9 +909,6 @@ def test_shift_kiritimati(self): 1995, 1, 1, 12, 30, tzinfo="Pacific/Kiritimati" ) - @pytest.mark.skipif( - sys.version_info < (3, 6), reason="unsupported before python 3.6" - ) def shift_imaginary_seconds(self): # offset has a seconds component monrovia = arrow.Arrow(1972, 1, 6, 23, tzinfo="Africa/Monrovia") diff --git a/tests/test_factory.py b/tests/test_factory.py index 2b8df5168..b954d1620 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import time from datetime import date, datetime @@ -20,8 +19,8 @@ def test_no_args(self): def test_timestamp_one_arg_no_arg(self): - no_arg = self.factory.get(1406430900).timestamp - one_arg = self.factory.get("1406430900", "X").timestamp + no_arg = self.factory.get(1406430900).timestamp() + one_arg = self.factory.get("1406430900", "X").timestamp() assert no_arg == one_arg @@ -289,7 +288,7 @@ def test_two_args_str_list(self): def test_two_args_unicode_unicode(self): - result = self.factory.get(u"2013-01-01", u"YYYY-MM-DD") + result = self.factory.get("2013-01-01", "YYYY-MM-DD") assert result._datetime == datetime(2013, 1, 1, tzinfo=tz.tzutc()) diff --git a/tests/test_formatter.py b/tests/test_formatter.py index e97aeb5dc..06831f1e0 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from datetime import datetime import pytest @@ -113,14 +112,13 @@ def test_sub_second(self): def test_timestamp(self): - timestamp = 1588437009.8952794 - dt = datetime.utcfromtimestamp(timestamp) - expected = str(int(timestamp)) + dt = datetime.now(tz=dateutil_tz.UTC) + expected = str(dt.timestamp()) assert self.formatter._format_token(dt, "X") == expected # Must round because time.time() may return a float with greater # than 6 digits of precision - expected = str(int(timestamp * 1000000)) + expected = str(int(dt.timestamp() * 1000000)) assert self.formatter._format_token(dt, "x") == expected def test_timezone(self): diff --git a/tests/test_locales.py b/tests/test_locales.py index 76323f379..642013ba5 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import pytest from arrow import arrow, locales diff --git a/tests/test_parser.py b/tests/test_parser.py index 9e497f504..8170a2f1d 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,9 +1,5 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import calendar import os -import sys import time from datetime import datetime @@ -226,24 +222,19 @@ def test_parse_timestamp(self): tz_utc = tz.tzutc() int_timestamp = int(time.time()) self.expected = datetime.fromtimestamp(int_timestamp, tz=tz_utc) - assert self.parser.parse("{:d}".format(int_timestamp), "X") == self.expected + assert self.parser.parse(f"{int_timestamp:d}", "X") == self.expected float_timestamp = time.time() self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - assert self.parser.parse("{:f}".format(float_timestamp), "X") == self.expected + assert self.parser.parse(f"{float_timestamp:f}", "X") == self.expected # test handling of ns timestamp (arrow will round to 6 digits regardless) self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - assert ( - self.parser.parse("{:f}123".format(float_timestamp), "X") == self.expected - ) + assert self.parser.parse(f"{float_timestamp:f}123", "X") == self.expected # test ps timestamp (arrow will round to 6 digits regardless) self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) - assert ( - self.parser.parse("{:f}123456".format(float_timestamp), "X") - == self.expected - ) + assert self.parser.parse(f"{float_timestamp:f}123456", "X") == self.expected # NOTE: negative timestamps cannot be handled by datetime on Window # Must use timedelta to handle them. ref: https://stackoverflow.com/questions/36179914 @@ -252,15 +243,13 @@ def test_parse_timestamp(self): negative_int_timestamp = -int_timestamp self.expected = datetime.fromtimestamp(negative_int_timestamp, tz=tz_utc) assert ( - self.parser.parse("{:d}".format(negative_int_timestamp), "X") - == self.expected + self.parser.parse(f"{negative_int_timestamp:d}", "X") == self.expected ) negative_float_timestamp = -float_timestamp self.expected = datetime.fromtimestamp(negative_float_timestamp, tz=tz_utc) assert ( - self.parser.parse("{:f}".format(negative_float_timestamp), "X") - == self.expected + self.parser.parse(f"{negative_float_timestamp:f}", "X") == self.expected ) # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will @@ -292,10 +281,10 @@ def test_parse_expanded_timestamp(self): assert self.parser.parse("{:d}".format(int(timestamp)), "x") == self.expected self.expected = datetime.fromtimestamp(round(timestamp, 3), tz=tz_utc) - assert self.parser.parse("{:d}".format(timestamp_milli), "x") == self.expected + assert self.parser.parse(f"{timestamp_milli:d}", "x") == self.expected self.expected = datetime.fromtimestamp(timestamp, tz=tz_utc) - assert self.parser.parse("{:d}".format(timestamp_micro), "x") == self.expected + assert self.parser.parse(f"{timestamp_micro:d}", "x") == self.expected # anything above max µs timestamp should fail with pytest.raises(ValueError): @@ -303,7 +292,7 @@ def test_parse_expanded_timestamp(self): # floats are not allowed with the "x" token with pytest.raises(ParserMatchError): - self.parser.parse("{:f}".format(timestamp), "x") + self.parser.parse(f"{timestamp:f}", "x") def test_parse_names(self): @@ -346,7 +335,7 @@ 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("2013-01-01 {}".format(full_tz_name), "YYYY-MM-DD ZZZ") + self.parser.parse(f"2013-01-01 {full_tz_name}", "YYYY-MM-DD ZZZ") == self.expected ) @@ -940,9 +929,7 @@ def test_time(self): for sep in time_seperators: assert time_re.findall("12") == [("12", "", "", "", "")] - assert time_re.findall("12{sep}35".format(sep=sep)) == [ - ("12", "35", "", "", "") - ] + assert time_re.findall(f"12{sep}35") == [("12", "35", "", "", "")] assert time_re.findall("12{sep}35{sep}46".format(sep=sep)) == [ ("12", "35", "46", "", "") ] @@ -1658,7 +1645,6 @@ def test_escape(self): ) == datetime(2017, 12, 31, 2, 0) -@pytest.mark.skipif(sys.version_info < (3, 5), reason="requires python3.5 or higher") @pytest.mark.usefixtures("dt_parser") class TestFuzzInput: # Regression test for issue #860 diff --git a/tests/test_util.py b/tests/test_util.py index e48b4de06..2d4bd1921 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import time from datetime import datetime diff --git a/tests/utils.py b/tests/utils.py index 2a048feb3..05ac5af4c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import pytz from dateutil.zoneinfo import get_zonefile_instance diff --git a/tox.ini b/tox.ini index c8e1a3e45..492b09eb7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,11 @@ [tox] minversion = 3.18.0 -envlist = py{py3,27,35,36,37,38,39} +envlist = py{py3,36,37,38,39} skip_missing_interpreters = true [gh-actions] python = pypy3: pypy3 - 2.7: py27 - 3.5: py35 3.6: py36 3.7: py37 3.8: py38 From f885241eb7e954a79d95a59b085fbe310c537c1c Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Wed, 18 Nov 2020 11:14:07 -0500 Subject: [PATCH 478/649] Fix MAX_TIMESTAMP on Windows (#882) * Fix max timestamp * Fix scoping * Catch OS Error also * Fix comment --- arrow/constants.py | 24 ++++++++++++++++++------ tests/test_parser.py | 42 ++++++++++++++++++++++-------------------- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/arrow/constants.py b/arrow/constants.py index d63698448..7c0a90080 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -1,12 +1,24 @@ -import os +import sys from datetime import datetime # datetime.max.timestamp() errors on Windows, so we must hardcode # the highest possible datetime value that can output a timestamp. # tl;dr platform-independent max timestamps are hard to form # See: https://stackoverflow.com/q/46133223 -MAX_TIMESTAMP = ( - datetime(3001, 1, 18, 23, 59, 59, 999999) if os.name == "nt" else datetime.max -).timestamp() -MAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1000 -MAX_TIMESTAMP_US = MAX_TIMESTAMP * 1000000 +try: + # Get max timestamp. Works on POSIX-based systems like Linux and macOS, + # but will trigger an OverflowError, ValueError, or OSError on Windows + MAX_TIMESTAMP = datetime.max.timestamp() +except (OverflowError, ValueError, OSError): # pragma: no cover + # Fallback for Windows if initial max timestamp call fails + # 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 + is_64bits = sys.maxsize > 2 ** 32 + MAX_TIMESTAMP = ( + datetime(3000, 12, 31, 23, 59, 59, 999999).timestamp() + if is_64bits + else datetime(2038, 1, 18, 23, 59, 59, 999999).timestamp() + ) + +MAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1e3 +MAX_TIMESTAMP_US = MAX_TIMESTAMP * 1e6 diff --git a/tests/test_parser.py b/tests/test_parser.py index 8170a2f1d..69ff4b6fc 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -220,11 +220,11 @@ def test_parse_year_two_digit(self): def test_parse_timestamp(self): tz_utc = tz.tzutc() - int_timestamp = int(time.time()) + float_timestamp = time.time() + int_timestamp = int(float_timestamp) self.expected = datetime.fromtimestamp(int_timestamp, tz=tz_utc) assert self.parser.parse(f"{int_timestamp:d}", "X") == self.expected - float_timestamp = time.time() self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) assert self.parser.parse(f"{float_timestamp:f}", "X") == self.expected @@ -236,22 +236,6 @@ def test_parse_timestamp(self): self.expected = datetime.fromtimestamp(float_timestamp, tz=tz_utc) assert self.parser.parse(f"{float_timestamp:f}123456", "X") == self.expected - # NOTE: negative timestamps cannot be handled by datetime on Window - # Must use timedelta to handle them. ref: https://stackoverflow.com/questions/36179914 - if os.name != "nt": - # regression test for issue #662 - negative_int_timestamp = -int_timestamp - self.expected = datetime.fromtimestamp(negative_int_timestamp, tz=tz_utc) - assert ( - self.parser.parse(f"{negative_int_timestamp:d}", "X") == self.expected - ) - - negative_float_timestamp = -float_timestamp - self.expected = datetime.fromtimestamp(negative_float_timestamp, tz=tz_utc) - assert ( - self.parser.parse(f"{negative_float_timestamp:f}", "X") == self.expected - ) - # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will # break cases like "15 Jul 2000" and a format list (see issue #447) with pytest.raises(ParserError): @@ -266,6 +250,24 @@ def test_parse_timestamp(self): with pytest.raises(ParserError): self.parser.parse(".1565982019", "X") + # NOTE: negative timestamps cannot be handled by datetime on Windows + # Must use timedelta to handle them: https://stackoverflow.com/questions/36179914 + @pytest.mark.skipif( + os.name == "nt", reason="negative timestamps are not supported on Windows" + ) + def test_parse_negative_timestamp(self): + # regression test for issue #662 + tz_utc = tz.tzutc() + float_timestamp = time.time() + int_timestamp = int(float_timestamp) + negative_int_timestamp = -int_timestamp + self.expected = datetime.fromtimestamp(negative_int_timestamp, tz=tz_utc) + assert self.parser.parse(f"{negative_int_timestamp:d}", "X") == self.expected + + negative_float_timestamp = -float_timestamp + self.expected = datetime.fromtimestamp(negative_float_timestamp, tz=tz_utc) + assert self.parser.parse(f"{negative_float_timestamp:f}", "X") == self.expected + def test_parse_expanded_timestamp(self): # test expanded timestamps that include milliseconds # and microseconds as multiples rather than decimals @@ -273,8 +275,8 @@ def test_parse_expanded_timestamp(self): tz_utc = tz.tzutc() timestamp = 1569982581.413132 - timestamp_milli = int(round(timestamp * 1000)) - timestamp_micro = int(round(timestamp * 1000000)) + timestamp_milli = int(round(timestamp * 1e3)) + timestamp_micro = int(round(timestamp * 1e6)) # "x" token should parse integer timestamps below MAX_TIMESTAMP normally self.expected = datetime.fromtimestamp(int(timestamp), tz=tz_utc) From 1278a21b46fcf145592f15b47a8169f97f54415a Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Wed, 25 Nov 2020 21:28:03 -0500 Subject: [PATCH 479/649] Audit errors and clean up utils (#887) * Fix MAX_TIMESTAMP on Windows (#882) * Fix max timestamp * Fix scoping * Catch OS Error also * Fix comment * Audit and improve error messages and remove total_seconds util function. --- arrow/arrow.py | 78 ++++++++++++++++++++------------------------- arrow/factory.py | 16 +++------- arrow/formatter.py | 4 +-- arrow/locales.py | 4 +-- arrow/parser.py | 25 +++++++-------- arrow/util.py | 9 ++---- tests/test_arrow.py | 18 +++++------ tests/test_util.py | 4 --- tests/utils.py | 4 +-- 9 files changed, 66 insertions(+), 96 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 899c680c9..e21d91973 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -573,7 +573,7 @@ def interval(cls, frame, start, end, interval=1, tz=None, bounds="[)"): (, ) """ if interval < 1: - raise ValueError("interval has to be a positive integer") + raise ValueError("Interval must be a positive integer.") spanRange = iter(cls.span_range(frame, start, end, tz, bounds=bounds)) while True: @@ -775,9 +775,9 @@ def replace(self, **kwargs): if key in self._ATTRS: absolute_kwargs[key] = value elif key in ["week", "quarter"]: - raise AttributeError(f"setting absolute {key} is not supported") + raise ValueError(f"Setting absolute {key} is not supported.") elif key not in ["tzinfo", "fold"]: - raise AttributeError(f'unknown attribute: "{key}"') + raise ValueError(f"Unknown attribute: '{key}'.") current = self._datetime.replace(**absolute_kwargs) @@ -833,10 +833,9 @@ def shift(self, **kwargs): if key in self._ATTRS_PLURAL or key in additional_attrs: relative_kwargs[key] = value else: - raise AttributeError( - "Invalid shift time frame. Please select one of the following: {}.".format( - ", ".join(self._ATTRS_PLURAL + additional_attrs) - ) + supported_attr = ", ".join(self._ATTRS_PLURAL + additional_attrs) + raise ValueError( + f"Invalid shift time frame. Please select one of the following: {supported_attr}." ) # core datetime does not support quarters, translate to months. @@ -966,16 +965,14 @@ def humanize( else: raise TypeError( - "Invalid 'other' argument of type '{}'. " - "Argument must be of type None, Arrow, or datetime.".format( - type(other).__name__ - ) + f"Invalid 'other' argument of type '{type(other).__name__}'. " + "Argument must be of type None, Arrow, or datetime." ) if isinstance(granularity, list) and len(granularity) == 1: granularity = granularity[0] - delta = int(round(util.total_seconds(self._datetime - dt))) + delta = int(round((self._datetime - dt).total_seconds())) sign = -1 if delta < 0 else 1 diff = abs(delta) delta = diff @@ -1053,8 +1050,9 @@ def humanize( elif granularity == "year": delta = sign * delta / self._SECS_PER_YEAR else: - raise AttributeError( - "Invalid level of granularity. Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'" + raise ValueError( + "Invalid level of granularity. " + "Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'." ) if trunc(abs(delta)) != 1: @@ -1098,7 +1096,7 @@ def humanize( timeframes.append(["second", seconds]) if len(timeframes) < len(granularity): - raise AttributeError( + raise ValueError( "Invalid level of granularity. " "Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'." ) @@ -1111,10 +1109,8 @@ def humanize( except KeyError as e: raise ValueError( - "Humanization of the {} granularity is not currently translated in the '{}' locale. " - "Please consider making a contribution to this locale.".format( - e, locale_name - ) + f"Humanization of the {e} granularity is not currently translated in the '{locale_name}' locale. " + "Please consider making a contribution to this locale." ) # query functions @@ -1153,13 +1149,11 @@ def is_between(self, start, end, bounds="()"): if not isinstance(start, Arrow): raise TypeError( - "Can't parse start date argument type of '{}'".format(type(start)) + f"Cannot parse start date argument type of '{type(start)}'." ) if not isinstance(end, Arrow): - raise TypeError( - "Can't parse end date argument type of '{}'".format(type(end)) - ) + raise TypeError(f"Cannot parse end date argument type of '{type(start)}'.") include_start = bounds[0] == "[" include_end = bounds[1] == "]" @@ -1477,7 +1471,7 @@ def _get_tzinfo(tz_expr): try: return parser.TzinfoParser.parse(tz_expr) except parser.ParserError: - raise ValueError(f"'{tz_expr}' not recognized as a timezone") + raise ValueError(f"'{tz_expr}' not recognized as a timezone.") @classmethod def _get_datetime(cls, expr): @@ -1503,25 +1497,23 @@ def _get_frames(cls, name): return "week", "weeks", 1 elif name in ["quarter", "quarters"]: return "quarter", "months", 3 - - supported = ", ".join( - [ - "year(s)", - "month(s)", - "day(s)", - "hour(s)", - "minute(s)", - "second(s)", - "microsecond(s)", - "week(s)", - "quarter(s)", - ] - ) - raise AttributeError( - "range/span over frame {} not supported. Supported frames: {}".format( - name, supported + else: + supported = ", ".join( + [ + "year(s)", + "month(s)", + "day(s)", + "hour(s)", + "minute(s)", + "second(s)", + "microsecond(s)", + "week(s)", + "quarter(s)", + ] + ) + raise ValueError( + f"Range or span over frame {name} not supported. Supported frames: {supported}." ) - ) @classmethod def _get_iteration_params(cls, end, limit): @@ -1529,7 +1521,7 @@ def _get_iteration_params(cls, end, limit): if end is None: if limit is None: - raise ValueError("one of 'end' or 'limit' is required") + raise ValueError("One of 'end' or 'limit' is required.") return cls.max, limit diff --git a/arrow/factory.py b/arrow/factory.py index 9bae80c58..77cc89ee4 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -207,9 +207,7 @@ def get(self, *args, **kwargs): return self.type.fromdate(dt) else: - raise TypeError( - "Can't parse single argument of type '{}'".format(type(arg)) - ) + raise TypeError(f"Cannot parse single argument of type '{type(arg)}'.") elif arg_count == 2: @@ -222,9 +220,7 @@ def get(self, *args, **kwargs): return self.type.fromdatetime(arg_1, arg_2) else: raise TypeError( - "Can't parse two arguments of types 'datetime', '{}'".format( - type(arg_2) - ) + f"Cannot parse two arguments of types 'datetime', '{type(arg_2)}'." ) elif isinstance(arg_1, date): @@ -234,9 +230,7 @@ def get(self, *args, **kwargs): return self.type.fromdate(arg_1, tzinfo=arg_2) else: raise TypeError( - "Can't parse two arguments of types 'date', '{}'".format( - type(arg_2) - ) + f"Cannot parse two arguments of types 'date', '{type(arg_2)}'." ) # (str, format) -> parse. @@ -250,9 +244,7 @@ def get(self, *args, **kwargs): else: raise TypeError( - "Can't parse two arguments of types '{}' and '{}'".format( - type(arg_1), type(arg_2) - ) + f"Cannot parse two arguments of types '{type(arg_1)}' and '{type(arg_2)}'." ) # 3+ args -> datetime-like via constructor. diff --git a/arrow/formatter.py b/arrow/formatter.py index 6bd61c89d..0b9ec0cc0 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -2,7 +2,7 @@ from dateutil import tz as dateutil_tz -from arrow import locales, util +from arrow import locales FORMAT_ATOM = "YYYY-MM-DD HH:mm:ssZZ" FORMAT_COOKIE = "dddd, DD-MMM-YYYY HH:mm:ss ZZZ" @@ -116,7 +116,7 @@ def _format_token(self, dt, token): if token in ["ZZ", "Z"]: separator = ":" if token == "ZZ" else "" tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo - total_minutes = int(util.total_seconds(tz.utcoffset(dt)) / 60) + total_minutes = int(tz.utcoffset(dt).total_seconds() / 60) sign = "+" if total_minutes >= 0 else "-" total_minutes = abs(total_minutes) diff --git a/arrow/locales.py b/arrow/locales.py index 3d0ca4637..7ead2cc5b 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -14,7 +14,7 @@ def get_locale(name): locale_cls = _locales.get(name.lower()) if locale_cls is None: - raise ValueError(f"Unsupported locale '{name}'") + raise ValueError(f"Unsupported locale '{name}'.") return locale_cls() @@ -29,7 +29,7 @@ def get_locale_by_class_name(name): locale_cls = globals().get(name) if locale_cls is None: - raise ValueError(f"Unsupported locale '{name}'") + raise ValueError(f"Unsupported locale '{name}'.") return locale_cls() diff --git a/arrow/parser.py b/arrow/parser.py index 21ff3335a..b528f3e48 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -118,9 +118,8 @@ def parse_iso(self, datetime_string, normalize_whitespace=False): num_spaces = datetime_string.count(" ") if has_space_divider and num_spaces != 1 or has_t_divider and num_spaces > 0: raise ParserError( - "Expected an ISO 8601-like string, but was given '{}'. Try passing in a format string to resolve this.".format( - datetime_string - ) + f"Expected an ISO 8601-like string, but was given '{datetime_string}'. " + "Try passing in a format string to resolve this." ) has_time = has_space_divider or has_t_divider @@ -161,7 +160,8 @@ def parse_iso(self, datetime_string, normalize_whitespace=False): if time_components is None: raise ParserError( - "Invalid time component provided. Please specify a format or provide a valid time component in the basic or extended ISO 8601 time format." + "Invalid time component provided. " + "Please specify a format or provide a valid time component in the basic or extended ISO 8601 time format." ) ( @@ -221,14 +221,14 @@ def parse(self, datetime_string, fmt, normalize_whitespace=False): fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt) except re.error as e: raise ParserMatchError( - f"Failed to generate regular expression pattern: {e}" + f"Failed to generate regular expression pattern: {e}." ) match = fmt_pattern_re.search(datetime_string) if match is None: raise ParserMatchError( - f"Failed to match '{fmt}' when parsing '{datetime_string}'" + f"Failed to match '{fmt}' when parsing '{datetime_string}'." ) parts = {} @@ -242,9 +242,7 @@ def parse(self, datetime_string, fmt, normalize_whitespace=False): if value is None: raise ParserMatchError( - "Unable to find a match group for the specified token '{}'.".format( - token - ) + f"Unable to find a match group for the specified token '{token}'." ) self._parse_token(token, value, parts) @@ -279,7 +277,7 @@ def _generate_pattern_re(self, fmt): try: input_re = self._input_re_map[token] except KeyError: - raise ParserError(f"Unrecognized token '{token}'") + raise ParserError(f"Unrecognized token '{token}'.") input_pattern = f"(?P<{token}>{input_re.pattern})" tokens.append(token) # a pattern doesn't have the same length as the token @@ -548,10 +546,9 @@ def _parse_multiformat(self, string, formats): pass if _datetime is None: + supported_formats = ", ".join(formats) raise ParserError( - "Could not match input '{}' to any of the following formats: {}".format( - string, ", ".join(formats) - ) + f"Could not match input '{string}' to any of the following formats: {supported_formats}." ) return _datetime @@ -595,6 +592,6 @@ def parse(cls, tzinfo_string): tzinfo = tz.gettz(tzinfo_string) if tzinfo is None: - raise ParserError(f'Could not parse timezone expression "{tzinfo_string}"') + raise ParserError(f"Could not parse timezone expression '{tzinfo_string}'.") return tzinfo diff --git a/arrow/util.py b/arrow/util.py index 7053a8863..55354a254 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -31,11 +31,6 @@ def next_weekday(start_date, weekday): return rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0] -def total_seconds(td): - """Get total seconds for timedelta.""" - return td.total_seconds() - - def is_timestamp(value): """Check if value is a valid timestamp.""" if isinstance(value, bool): @@ -85,8 +80,8 @@ def iso_to_gregorian(iso_year, iso_week, iso_day): def validate_bounds(bounds): if bounds != "()" and bounds != "(]" and bounds != "[)" and bounds != "[]": raise ValueError( - 'Invalid bounds. Please select between "()", "(]", "[)", or "[]".' + "Invalid bounds. Please select between '()', '(]', '[)', or '[]'." ) -__all__ = ["next_weekday", "total_seconds", "is_timestamp", "iso_to_gregorian"] +__all__ = ["next_weekday", "is_timestamp", "iso_to_gregorian"] diff --git a/tests/test_arrow.py b/tests/test_arrow.py index fe583d98b..b235c26c9 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -659,7 +659,7 @@ def test_pickle_and_unpickle(self): class TestArrowReplace: def test_not_attr(self): - with pytest.raises(AttributeError): + with pytest.raises(ValueError): arrow.Arrow.utcnow().replace(abc=1) def test_replace(self): @@ -700,12 +700,12 @@ def test_replace_fold_and_other(self): def test_replace_week(self): - with pytest.raises(AttributeError): + with pytest.raises(ValueError): arrow.Arrow.utcnow().replace(week=1) def test_replace_quarter(self): - with pytest.raises(AttributeError): + with pytest.raises(ValueError): arrow.Arrow.utcnow().replace(quarter=1) def test_replace_quarter_and_fold(self): @@ -726,10 +726,10 @@ def test_not_attr(self): now = arrow.Arrow.utcnow() - with pytest.raises(AttributeError): + with pytest.raises(ValueError): now.shift(abc=1) - with pytest.raises(AttributeError): + with pytest.raises(ValueError): now.shift(week=1) def test_shift(self): @@ -1119,7 +1119,7 @@ def test_imaginary(self): def test_unsupported(self): - with pytest.raises(AttributeError): + with pytest.raises(ValueError): next(arrow.Arrow.range("abc", datetime.utcnow(), datetime.utcnow())) def test_range_over_months_ending_on_different_days(self): @@ -1510,7 +1510,7 @@ def test_bounds_param_is_passed(self): class TestArrowSpan: def test_span_attribute(self): - with pytest.raises(AttributeError): + with pytest.raises(ValueError): self.arrow.span("span") def test_span_year(self): @@ -1700,7 +1700,7 @@ def test_granularity(self): == "3 years" ) - with pytest.raises(AttributeError): + with pytest.raises(ValueError): self.now.humanize(later108, granularity="years") def test_multiple_granularity(self): @@ -1740,7 +1740,7 @@ def test_multiple_granularity(self): self.now.humanize(later105, granularity=["hour", "day", "minute"]) == "a day 3 hours and 46 minutes ago" ) - with pytest.raises(AttributeError): + with pytest.raises(ValueError): self.now.humanize(later105, granularity=["error", "second"]) later108onlydistance = self.now.shift(seconds=10 ** 8) diff --git a/tests/test_util.py b/tests/test_util.py index 2d4bd1921..2d8c11ed0 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -36,10 +36,6 @@ def test_next_weekday(self): with pytest.raises(ValueError): util.next_weekday(datetime(1970, 1, 1), -1) - def test_total_seconds(self): - td = datetime(2019, 1, 1) - datetime(2018, 1, 1) - assert util.total_seconds(td) == td.total_seconds() - def test_is_timestamp(self): timestamp_float = time.time() timestamp_int = int(timestamp_float) diff --git a/tests/utils.py b/tests/utils.py index 05ac5af4c..95b47c166 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,8 +1,6 @@ import pytz from dateutil.zoneinfo import get_zonefile_instance -from arrow import util - def make_full_tz_list(): dateutil_zones = set(get_zonefile_instance().zones) @@ -12,4 +10,4 @@ def make_full_tz_list(): def assert_datetime_equality(dt1, dt2, within=10): assert dt1.tzinfo == dt2.tzinfo - assert abs(util.total_seconds(dt1 - dt2)) < within + assert abs((dt1 - dt2).total_seconds()) < within From 0aebf5729341c28926cca5ab0e2fec9737163ba6 Mon Sep 17 00:00:00 2001 From: Yiran Si <44989609+yiransii@users.noreply.github.com> Date: Wed, 2 Dec 2020 14:57:51 -0500 Subject: [PATCH 480/649] Use datetime.strptime tokens internally for parsing ISO 8601 week dates (#889) * a small test to play with source code * replace iso_to_gregorian method in parser with datetime.strptime * remove import iso_to_gregorian * a small test to play with source code * replace iso_to_gregorian method in parser with datetime.strptime * remove import iso_to_gregorian * use tokens for ISO 8601 --- arrow/parser.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/arrow/parser.py b/arrow/parser.py index b528f3e48..13b0b2ebf 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -5,7 +5,7 @@ from dateutil import tz from arrow import locales -from arrow.util import iso_to_gregorian, next_weekday, normalize_timestamp +from arrow.util import next_weekday, normalize_timestamp class ParserError(ValueError): @@ -415,7 +415,7 @@ def _build_datetime(parts): weekdate = parts.get("weekdate") if weekdate is not None: - # we can use strptime (%G, %V, %u) in python 3.6 but these tokens aren't available before that + year, week = int(weekdate[0]), int(weekdate[1]) if weekdate[2] is not None: @@ -424,7 +424,11 @@ def _build_datetime(parts): # day not given, default to 1 day = 1 - dt = iso_to_gregorian(year, week, day) + date_string = f"{year}-{week}-{day}" + + # tokens for ISO 8601 weekdates + dt = datetime.strptime(date_string, "%G-%V-%u") + parts["year"] = dt.year parts["month"] = dt.month parts["day"] = dt.day From e521f2777d8d6569df26b78bdf9ae5ccc89217e8 Mon Sep 17 00:00:00 2001 From: Vanita Sharma <44988599+svanita00@users.noreply.github.com> Date: Thu, 3 Dec 2020 06:09:12 -0500 Subject: [PATCH 481/649] Add 'exact' keyword in span_range, span and interval methods (#888) * Added keyword exact to functions * Added tests for edge cases in span range * Fixed lint docs error and span docstring. * Update arrow/arrow.py to avoid excess indentation Co-authored-by: Chris <30196510+systemcatch@users.noreply.github.com> * Added tests to span function with exact and bounds specified Co-authored-by: Chris <30196510+systemcatch@users.noreply.github.com> --- arrow/arrow.py | 69 +++++++++++---- tests/test_arrow.py | 206 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+), 18 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index e21d91973..9b9d110fe 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -377,7 +377,7 @@ def range(cls, frame, start, end=None, tz=None, limit=None): if day_is_clipped and not cls._is_last_day_of_month(current): current = current.replace(day=original_day) - def span(self, frame, count=1, bounds="[)"): + def span(self, frame, count=1, bounds="[)", exact=False): """Returns two new :class:`Arrow ` objects, representing the timespan of the :class:`Arrow ` object in a given timeframe. @@ -387,6 +387,9 @@ def span(self, frame, count=1, bounds="[)"): whether to include or exclude the start and end values in the span. '(' excludes the start, '[' includes the start, ')' excludes the end, and ']' includes the end. If the bounds are not specified, the default bound '[)' is used. + :param exact: (optional) whether to have the start of the timespan begin exactly + at the time specified by ``start`` and the end of the timespan truncated + so as not to extend beyond ``end``. Supported frame values: year, quarter, month, week, day, hour, minute, second. @@ -420,20 +423,22 @@ def span(self, frame, count=1, bounds="[)"): else: attr = frame_absolute - index = self._ATTRS.index(attr) - frames = self._ATTRS[: index + 1] + floor = self + if not exact: + index = self._ATTRS.index(attr) + frames = self._ATTRS[: index + 1] - values = [getattr(self, f) for f in frames] + values = [getattr(self, f) for f in frames] - for _ in range(3 - len(values)): - values.append(1) + for _ in range(3 - len(values)): + values.append(1) - floor = self.__class__(*values, tzinfo=self.tzinfo) + floor = self.__class__(*values, tzinfo=self.tzinfo) - if frame_absolute == "week": - floor = floor.shift(days=-(self.isoweekday() - 1)) - elif frame_absolute == "quarter": - floor = floor.shift(months=-((self.month - 1) % 3)) + if frame_absolute == "week": + floor = floor.shift(days=-(self.isoweekday() - 1)) + elif frame_absolute == "quarter": + floor = floor.shift(months=-((self.month - 1) % 3)) ceil = floor.shift(**{frame_relative: count * relative_steps}) @@ -478,7 +483,9 @@ def ceil(self, frame): return self.span(frame)[1] @classmethod - def span_range(cls, frame, start, end, tz=None, limit=None, bounds="[)"): + def span_range( + cls, frame, start, end, tz=None, limit=None, bounds="[)", exact=False + ): """Returns an iterator of tuples, each :class:`Arrow ` objects, representing a series of timespans between two inputs. @@ -492,6 +499,9 @@ def span_range(cls, frame, start, end, tz=None, limit=None, bounds="[)"): whether to include or exclude the start and end values in each span in the range. '(' excludes the start, '[' includes the start, ')' excludes the end, and ']' includes the end. If the bounds are not specified, the default bound '[)' is used. + :param exact: (optional) whether to have the first timespan start exactly + at the time specified by ``start`` and the final span truncated + so as not to extend beyond ``end``. **NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to return the entire range. Call with ``limit`` alone to return a maximum # of results from @@ -528,12 +538,27 @@ def span_range(cls, frame, start, end, tz=None, limit=None, bounds="[)"): """ tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) - start = cls.fromdatetime(start, tzinfo).span(frame)[0] + start = cls.fromdatetime(start, tzinfo).span(frame, exact=exact)[0] + end = cls.fromdatetime(end, tzinfo) _range = cls.range(frame, start, end, tz, limit) - return (r.span(frame, bounds=bounds) for r in _range) + if not exact: + for r in _range: + yield r.span(frame, bounds=bounds, exact=exact) + + for r in _range: + floor, ceil = r.span(frame, bounds=bounds, exact=exact) + if ceil > end: + ceil = end + if bounds[1] == ")": + ceil += relativedelta(microseconds=-1) + if floor == end: + break + elif floor + relativedelta(microseconds=-1) == end: + break + yield floor, ceil @classmethod - def interval(cls, frame, start, end, interval=1, tz=None, bounds="[)"): + def interval(cls, frame, start, end, interval=1, tz=None, bounds="[)", exact=False): """Returns an iterator of tuples, each :class:`Arrow ` objects, representing a series of intervals between two inputs. @@ -546,6 +571,9 @@ def interval(cls, frame, start, end, interval=1, tz=None, bounds="[)"): whether to include or exclude the start and end values in the intervals. '(' excludes the start, '[' includes the start, ')' excludes the end, and ']' includes the end. If the bounds are not specified, the default bound '[)' is used. + :param exact: (optional) whether to have the first timespan start exactly + at the time specified by ``start`` and the final interval truncated + so as not to extend beyond ``end``. Supported frame values: year, quarter, month, week, day, hour, minute, second @@ -573,14 +601,19 @@ def interval(cls, frame, start, end, interval=1, tz=None, bounds="[)"): (, ) """ if interval < 1: - raise ValueError("Interval must be a positive integer.") + raise ValueError("interval has to be a positive integer") - spanRange = iter(cls.span_range(frame, start, end, tz, bounds=bounds)) + spanRange = iter( + cls.span_range(frame, start, end, tz, bounds=bounds, exact=exact) + ) while True: try: intvlStart, intvlEnd = next(spanRange) for _ in range(interval - 1): - _, intvlEnd = next(spanRange) + try: + _, intvlEnd = next(spanRange) + except StopIteration: + continue yield intvlStart, intvlEnd except StopIteration: return diff --git a/tests/test_arrow.py b/tests/test_arrow.py index b235c26c9..95137de45 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -1456,6 +1456,150 @@ def test_bounds_param_is_passed(self): (arrow.Arrow(2013, 4, 1), arrow.Arrow(2013, 7, 1)), ] + def test_exact_bound_exclude(self): + + result = list( + arrow.Arrow.span_range( + "hour", + datetime(2013, 5, 5, 12, 30), + datetime(2013, 5, 5, 17, 15), + bounds="[)", + exact=True, + ) + ) + + expected = [ + ( + arrow.Arrow(2013, 5, 5, 12, 30), + arrow.Arrow(2013, 5, 5, 13, 29, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 13, 30), + arrow.Arrow(2013, 5, 5, 14, 29, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 14, 30), + arrow.Arrow(2013, 5, 5, 15, 29, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 15, 30), + arrow.Arrow(2013, 5, 5, 16, 29, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 16, 30), + arrow.Arrow(2013, 5, 5, 17, 14, 59, 999999), + ), + ] + + assert result == expected + + def test_exact_floor_equals_end(self): + result = list( + arrow.Arrow.span_range( + "minute", + datetime(2013, 5, 5, 12, 30), + datetime(2013, 5, 5, 12, 40), + exact=True, + ) + ) + + expected = [ + ( + arrow.Arrow(2013, 5, 5, 12, 30), + arrow.Arrow(2013, 5, 5, 12, 30, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 12, 31), + arrow.Arrow(2013, 5, 5, 12, 31, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 12, 32), + arrow.Arrow(2013, 5, 5, 12, 32, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 12, 33), + arrow.Arrow(2013, 5, 5, 12, 33, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 12, 34), + arrow.Arrow(2013, 5, 5, 12, 34, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 12, 35), + arrow.Arrow(2013, 5, 5, 12, 35, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 12, 36), + arrow.Arrow(2013, 5, 5, 12, 36, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 12, 37), + arrow.Arrow(2013, 5, 5, 12, 37, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 12, 38), + arrow.Arrow(2013, 5, 5, 12, 38, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 12, 39), + arrow.Arrow(2013, 5, 5, 12, 39, 59, 999999), + ), + ] + + assert result == expected + + def test_exact_bound_include(self): + result = list( + arrow.Arrow.span_range( + "hour", + datetime(2013, 5, 5, 2, 30), + datetime(2013, 5, 5, 6, 00), + bounds="(]", + exact=True, + ) + ) + + expected = [ + ( + arrow.Arrow(2013, 5, 5, 2, 30, 00, 1), + arrow.Arrow(2013, 5, 5, 3, 30, 00, 0), + ), + ( + arrow.Arrow(2013, 5, 5, 3, 30, 00, 1), + arrow.Arrow(2013, 5, 5, 4, 30, 00, 0), + ), + ( + arrow.Arrow(2013, 5, 5, 4, 30, 00, 1), + arrow.Arrow(2013, 5, 5, 5, 30, 00, 0), + ), + ( + arrow.Arrow(2013, 5, 5, 5, 30, 00, 1), + arrow.Arrow(2013, 5, 5, 6, 00), + ), + ] + + assert result == expected + + def test_small_interval_exact_open_bounds(self): + result = list( + arrow.Arrow.span_range( + "minute", + datetime(2013, 5, 5, 2, 30), + datetime(2013, 5, 5, 2, 31), + bounds="()", + exact=True, + ) + ) + + expected = [ + ( + arrow.Arrow(2013, 5, 5, 2, 30, 00, 1), + arrow.Arrow(2013, 5, 5, 2, 30, 59, 999999), + ), + ] + + assert result == expected + class TestArrowInterval: def test_incorrect_input(self): @@ -1505,6 +1649,30 @@ def test_bounds_param_is_passed(self): (arrow.Arrow(2013, 5, 5, 16), arrow.Arrow(2013, 5, 5, 18)), ] + def test_exact(self): + result = list( + arrow.Arrow.interval( + "hour", + datetime(2013, 5, 5, 12, 30), + datetime(2013, 5, 5, 17, 15), + 4, + exact=True, + ) + ) + + expected = [ + ( + arrow.Arrow(2013, 5, 5, 12, 30), + arrow.Arrow(2013, 5, 5, 16, 29, 59, 999999), + ), + ( + arrow.Arrow(2013, 5, 5, 16, 30), + arrow.Arrow(2013, 5, 5, 17, 14, 59, 999999), + ), + ] + + assert result == expected + @pytest.mark.usefixtures("time_2013_02_15") class TestArrowSpan: @@ -1623,6 +1791,44 @@ 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()) + expected_ceil = datetime(2013, 2, 15, 4, 41, 22, 8922, tzinfo=tz.tzutc()) + + assert result_floor == expected_floor + 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()) + assert ceil == datetime(2013, 3, 1, 3, 41, 22, 8922, tzinfo=tz.tzutc()) + @pytest.mark.usefixtures("time_2013_01_01") class TestArrowHumanize: From 67732ee6f18c8208e2077f1bbb42e34bbd560c08 Mon Sep 17 00:00:00 2001 From: "Parshva D. Gogri" <71665911+pgogri@users.noreply.github.com> Date: Mon, 14 Dec 2020 08:34:48 +0530 Subject: [PATCH 482/649] Implemented new fromordinal method and added new timespec arg to isoformat. (#891) * added is_ordinal+tests and timespec arg. * added fromordinal and tests, also added to test for is_ordinal. * Update arrow/util.py changes the check to see if ordinal has a valid value. Co-authored-by: Chris <30196510+systemcatch@users.noreply.github.com> * Removed support for string ordinals, modified is_ordinal to throw correct exceptions, updated the tests. * Changed the function name from is_ordinal to validate_ordinal in util.py and converted it to a void function, updated the corresponding tests. This provides more clarity and consistency. * Made error messages consistent, fixed asserts in tests to look cleaner. Co-authored-by: Chris <30196510+systemcatch@users.noreply.github.com> --- arrow/arrow.py | 32 +++++++++++++++++++++++-- arrow/constants.py | 3 +++ arrow/util.py | 18 ++++++++++++-- tests/test_arrow.py | 32 +++++++++++++++++++++++++ tests/test_util.py | 57 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 4 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 9b9d110fe..838ff8888 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -295,6 +295,34 @@ def strptime(cls, date_str, fmt, tzinfo=None): fold=getattr(dt, "fold", 0), ) + @classmethod + def fromordinal(cls, ordinal): + """Constructs an :class:`Arrow ` object corresponding + to the Gregorian Ordinal. + + :param ordinal: an ``int`` corresponding to a Gregorian Ordinal. + + Usage:: + + >>> arrow.fromordinal(737741) + + + """ + + util.validate_ordinal(ordinal) + dt = datetime.fromordinal(ordinal) + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + dt.tzinfo, + fold=getattr(dt, "fold", 0), + ) + # factories: ranges and spans @classmethod @@ -1365,7 +1393,7 @@ def isocalendar(self): return self._datetime.isocalendar() - def isoformat(self, sep="T"): + def isoformat(self, sep="T", timespec="auto"): """Returns an ISO 8601 formatted representation of the date and time. Usage:: @@ -1375,7 +1403,7 @@ def isoformat(self, sep="T"): """ - return self._datetime.isoformat(sep) + return self._datetime.isoformat(sep, timespec) def ctime(self): """Returns a ctime formatted representation of the date and time. diff --git a/arrow/constants.py b/arrow/constants.py index 7c0a90080..830a45f07 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -22,3 +22,6 @@ MAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1e3 MAX_TIMESTAMP_US = MAX_TIMESTAMP * 1e6 + +MAX_ORDINAL = datetime.max.toordinal() +MIN_ORDINAL = 1 diff --git a/arrow/util.py b/arrow/util.py index 55354a254..6e9524594 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -2,7 +2,13 @@ from dateutil.rrule import WEEKLY, rrule -from arrow.constants import MAX_TIMESTAMP, MAX_TIMESTAMP_MS, MAX_TIMESTAMP_US +from arrow.constants import ( + MAX_ORDINAL, + MAX_TIMESTAMP, + MAX_TIMESTAMP_MS, + MAX_TIMESTAMP_US, + MIN_ORDINAL, +) def next_weekday(start_date, weekday): @@ -46,6 +52,14 @@ def is_timestamp(value): return False +def validate_ordinal(value): + """Raise the corresponding exception if value is an invalid Gregorian ordinal.""" + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"Ordinal must be an integer (got type {type(value)}).") + if not (MIN_ORDINAL <= value <= MAX_ORDINAL): + raise ValueError(f"Ordinal {value} is out of range.") + + def normalize_timestamp(timestamp): """Normalize millisecond and microsecond timestamps into normal timestamps.""" if timestamp > MAX_TIMESTAMP: @@ -84,4 +98,4 @@ def validate_bounds(bounds): ) -__all__ = ["next_weekday", "is_timestamp", "iso_to_gregorian"] +__all__ = ["next_weekday", "is_timestamp", "validate_ordinal", "iso_to_gregorian"] diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 95137de45..e73e3c2fa 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -182,6 +182,24 @@ def test_strptime(self): 2013, 2, 3, 12, 30, 45, tzinfo=tz.gettz("Europe/Paris") ) + def test_fromordinal(self): + + timestamp = 1607066909.937968 + with pytest.raises(TypeError): + arrow.Arrow.fromordinal(timestamp) + with pytest.raises(ValueError): + arrow.Arrow.fromordinal(int(timestamp)) + + ordinal = arrow.Arrow.utcnow().toordinal() + + with pytest.raises(TypeError): + arrow.Arrow.fromordinal(str(ordinal)) + + result = arrow.Arrow.fromordinal(ordinal) + dt = datetime.fromordinal(ordinal) + + assert result.naive == dt + @pytest.mark.usefixtures("time_2013_02_03") class TestTestArrowRepresentation: @@ -509,6 +527,20 @@ def test_isoformat(self): 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") + + result = self.arrow.isoformat(timespec="microseconds") + assert result == self.arrow._datetime.isoformat() + + result = self.arrow.isoformat(timespec="milliseconds") + assert result == self.arrow._datetime.isoformat(timespec="milliseconds") + + result = self.arrow.isoformat(sep="x", timespec="seconds") + 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) diff --git a/tests/test_util.py b/tests/test_util.py index 2d8c11ed0..3b32e1bc5 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -56,6 +56,63 @@ class InvalidTimestamp: full_datetime = "2019-06-23T13:12:42" assert not util.is_timestamp(full_datetime) + def test_validate_ordinal(self): + timestamp_float = 1607066816.815537 + timestamp_int = int(timestamp_float) + timestamp_str = str(timestamp_int) + + with pytest.raises(TypeError): + util.validate_ordinal(timestamp_float) + with pytest.raises(TypeError): + util.validate_ordinal(timestamp_str) + with pytest.raises(TypeError): + util.validate_ordinal(True) + with pytest.raises(TypeError): + util.validate_ordinal(False) + + with pytest.raises(ValueError): + util.validate_ordinal(timestamp_int) + with pytest.raises(ValueError): + util.validate_ordinal(-1 * timestamp_int) + with pytest.raises(ValueError): + util.validate_ordinal(0) + + try: + util.validate_ordinal(1) + except (ValueError, TypeError) as exp: + pytest.fail(f"Exception raised when shouldn't have ({type(exp)}).") + + try: + util.validate_ordinal(datetime.max.toordinal()) + except (ValueError, TypeError) as exp: + pytest.fail(f"Exception raised when shouldn't have ({type(exp)}).") + + ordinal = datetime.utcnow().toordinal() + ordinal_str = str(ordinal) + ordinal_float = float(ordinal) + 0.5 + + with pytest.raises(TypeError): + util.validate_ordinal(ordinal_str) + with pytest.raises(TypeError): + util.validate_ordinal(ordinal_float) + with pytest.raises(ValueError): + util.validate_ordinal(-1 * ordinal) + + try: + util.validate_ordinal(ordinal) + except (ValueError, TypeError) as exp: + pytest.fail(f"Exception raised when shouldn't have ({type(exp)}).") + + full_datetime = "2019-06-23T13:12:42" + + class InvalidOrdinal: + pass + + with pytest.raises(TypeError): + util.validate_ordinal(InvalidOrdinal()) + with pytest.raises(TypeError): + util.validate_ordinal(full_datetime) + def test_normalize_timestamp(self): timestamp = 1591161115.194556 millisecond_timestamp = 1591161115194 From ea9f325dea89194fbd07cf74ad34a7e95f65ae4f Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Tue, 22 Dec 2020 23:19:00 +1100 Subject: [PATCH 483/649] Fix simple docs typo, inpute -> input (#901) There is a small typo in arrow/locales.py. Should read `input` rather than `inpute`. --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index 7ead2cc5b..24d24b00d 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -5,7 +5,7 @@ def get_locale(name): """Returns an appropriate :class:`Locale ` - corresponding to an inpute locale name. + corresponding to an input locale name. :param name: the name of the locale. From c3085672b7e33a28c54e497f27af04d521cf923e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Thu, 24 Dec 2020 18:16:43 +0100 Subject: [PATCH 484/649] Modernize code for Python 3.6+ (#903) * simplify chained conditions * Fix method docstrings * Convert map to dict comprehension * Refactor the describe_multi method * Better repr of strings * Refactor multiple isinstance --- arrow/arrow.py | 47 ++++++++++++++++++----------------------------- arrow/factory.py | 16 +++++++--------- arrow/locales.py | 27 +++++++++++++-------------- arrow/parser.py | 14 +++++++------- arrow/util.py | 6 ++---- 5 files changed, 47 insertions(+), 63 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 838ff8888..b590d6831 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -169,7 +169,7 @@ def fromtimestamp(cls, timestamp, tzinfo=None): tzinfo = parser.TzinfoParser.parse(tzinfo) if not util.is_timestamp(timestamp): - raise ValueError(f"The provided timestamp '{timestamp}' is invalid.") + raise ValueError(f"The provided timestamp {timestamp!r} is invalid.") timestamp = util.normalize_timestamp(float(timestamp)) dt = datetime.fromtimestamp(timestamp, tzinfo) @@ -195,7 +195,7 @@ def utcfromtimestamp(cls, timestamp): """ if not util.is_timestamp(timestamp): - raise ValueError(f"The provided timestamp '{timestamp}' is invalid.") + raise ValueError(f"The provided timestamp {timestamp!r} is invalid.") timestamp = util.normalize_timestamp(float(timestamp)) dt = datetime.utcfromtimestamp(timestamp) @@ -838,7 +838,7 @@ def replace(self, **kwargs): elif key in ["week", "quarter"]: raise ValueError(f"Setting absolute {key} is not supported.") elif key not in ["tzinfo", "fold"]: - raise ValueError(f"Unknown attribute: '{key}'.") + raise ValueError(f"Unknown attribute: {key!r}.") current = self._datetime.replace(**absolute_kwargs) @@ -965,6 +965,7 @@ def format(self, fmt="YYYY-MM-DD HH:mm:ssZZ", locale="en_us"): formatted according to a format string. :param fmt: the format string. + :param locale: the locale to format. Usage:: @@ -1026,7 +1027,7 @@ def humanize( else: raise TypeError( - f"Invalid 'other' argument of type '{type(other).__name__}'. " + f"Invalid 'other' argument of type {type(other).__name__!r}. " "Argument must be of type None, Arrow, or datetime." ) @@ -1170,7 +1171,7 @@ def humanize( except KeyError as e: raise ValueError( - f"Humanization of the {e} granularity is not currently translated in the '{locale_name}' locale. " + f"Humanization of the {e} granularity is not currently translated in the {locale_name!r} locale. " "Please consider making a contribution to this locale." ) @@ -1210,36 +1211,24 @@ def is_between(self, start, end, bounds="()"): if not isinstance(start, Arrow): raise TypeError( - f"Cannot parse start date argument type of '{type(start)}'." + f"Cannot parse start date argument type of {type(start)!r}." ) if not isinstance(end, Arrow): - raise TypeError(f"Cannot parse end date argument type of '{type(start)}'.") + raise TypeError(f"Cannot parse end date argument type of {type(start)!r}.") include_start = bounds[0] == "[" include_end = bounds[1] == "]" - target_timestamp = self.float_timestamp - start_timestamp = start.float_timestamp - end_timestamp = end.float_timestamp + target_ts = self.float_timestamp + start_ts = start.float_timestamp + end_ts = end.float_timestamp - if include_start and include_end: - return ( - target_timestamp >= start_timestamp - and target_timestamp <= end_timestamp - ) - elif include_start and not include_end: - return ( - target_timestamp >= start_timestamp and target_timestamp < end_timestamp - ) - elif not include_start and include_end: - return ( - target_timestamp > start_timestamp and target_timestamp <= end_timestamp - ) - else: - return ( - target_timestamp > start_timestamp and target_timestamp < end_timestamp - ) + return ( + (start_ts <= target_ts <= end_ts) + and (include_start or start_ts < target_ts) + and (include_end or target_ts < end_ts) + ) # datetime methods @@ -1532,7 +1521,7 @@ def _get_tzinfo(tz_expr): try: return parser.TzinfoParser.parse(tz_expr) except parser.ParserError: - raise ValueError(f"'{tz_expr}' not recognized as a timezone.") + raise ValueError(f"{tz_expr!r} not recognized as a timezone.") @classmethod def _get_datetime(cls, expr): @@ -1545,7 +1534,7 @@ def _get_datetime(cls, expr): timestamp = float(expr) return cls.utcfromtimestamp(timestamp).datetime else: - raise ValueError(f"'{expr}' not recognized as a datetime or timestamp.") + raise ValueError(f"{expr!r} not recognized as a datetime or timestamp.") @classmethod def _get_frames(cls, name): diff --git a/arrow/factory.py b/arrow/factory.py index 77cc89ee4..b25170498 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -207,7 +207,7 @@ def get(self, *args, **kwargs): return self.type.fromdate(dt) else: - raise TypeError(f"Cannot parse single argument of type '{type(arg)}'.") + raise TypeError(f"Cannot parse single argument of type {type(arg)!r}.") elif arg_count == 2: @@ -216,27 +216,25 @@ def get(self, *args, **kwargs): if isinstance(arg_1, datetime): # (datetime, tzinfo/str) -> fromdatetime replace tzinfo. - if isinstance(arg_2, dt_tzinfo) or isinstance(arg_2, str): + if isinstance(arg_2, (dt_tzinfo, str)): return self.type.fromdatetime(arg_1, arg_2) else: raise TypeError( - f"Cannot parse two arguments of types 'datetime', '{type(arg_2)}'." + f"Cannot parse two arguments of types 'datetime', {type(arg_2)!r}." ) elif isinstance(arg_1, date): # (date, tzinfo/str) -> fromdate replace tzinfo. - if isinstance(arg_2, dt_tzinfo) or isinstance(arg_2, str): + if isinstance(arg_2, (dt_tzinfo, str)): return self.type.fromdate(arg_1, tzinfo=arg_2) else: raise TypeError( - f"Cannot parse two arguments of types 'date', '{type(arg_2)}'." + f"Cannot parse two arguments of types 'date', {type(arg_2)!r}." ) # (str, format) -> parse. - elif isinstance(arg_1, str) and ( - isinstance(arg_2, str) or isinstance(arg_2, list) - ): + elif isinstance(arg_1, str) and isinstance(arg_2, (str, list)): dt = parser.DateTimeParser(locale).parse( args[0], args[1], normalize_whitespace ) @@ -244,7 +242,7 @@ def get(self, *args, **kwargs): else: raise TypeError( - f"Cannot parse two arguments of types '{type(arg_1)}' and '{type(arg_2)}'." + f"Cannot parse two arguments of types {type(arg_1)!r} and {type(arg_2)!r}." ) # 3+ args -> datetime-like via constructor. diff --git a/arrow/locales.py b/arrow/locales.py index 24d24b00d..3cad6b07e 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -14,7 +14,7 @@ def get_locale(name): locale_cls = _locales.get(name.lower()) if locale_cls is None: - raise ValueError(f"Unsupported locale '{name}'.") + raise ValueError(f"Unsupported locale {name!r}.") return locale_cls() @@ -29,7 +29,7 @@ def get_locale_by_class_name(name): locale_cls = globals().get(name) if locale_cls is None: - raise ValueError(f"Unsupported locale '{name}'.") + raise ValueError(f"Unsupported locale {name!r}.") return locale_cls() @@ -99,16 +99,15 @@ def describe_multi(self, timeframes, only_distance=False): :param only_distance: return only distance eg: "2 hours and 11 seconds" without "in" or "ago" keywords """ - humanized = "" - for index, (timeframe, delta) in enumerate(timeframes): - humanized += self._format_timeframe(timeframe, delta) - if index == len(timeframes) - 2 and self.and_word: - humanized += " " + self.and_word + " " - elif index < len(timeframes) - 1: - humanized += " " + parts = [ + self._format_timeframe(timeframe, delta) for timeframe, delta in timeframes + ] + if self.and_word: + parts.insert(-1, self.and_word) + humanized = " ".join(parts) if not only_distance: - humanized = self._format_relative(humanized, timeframe, delta) + humanized = self._format_relative(humanized, *timeframes[-1]) return humanized @@ -166,14 +165,14 @@ def month_number(self, name): def year_full(self, year): """Returns the year for specific locale if available - :param name: the ``int`` year (4-digit) + :param year: the ``int`` year (4-digit) """ return f"{year:04d}" def year_abbreviation(self, year): """Returns the year for specific locale if available - :param name: the ``int`` year (4-digit) + :param year: the ``int`` year (4-digit) """ return f"{year:04d}"[2:] @@ -200,7 +199,7 @@ def _ordinal_number(self, n): return f"{n}" def _name_to_ordinal(self, lst): - return dict(map(lambda i: (i[1].lower(), i[0] + 1), enumerate(lst[1:]))) + return {elem.lower(): i for i, elem in enumerate(lst[1:], 1)} def _format_timeframe(self, timeframe, delta): return self.timeframes[timeframe].format(trunc(abs(delta))) @@ -2421,7 +2420,7 @@ def _format_timeframe(self, timeframe, delta): if isinstance(form, dict): if delta == 2: form = form["double"] - elif delta > 2 and delta <= 10: + elif 2 < delta <= 10: form = form["ten"] else: form = form["higher"] diff --git a/arrow/parser.py b/arrow/parser.py index 13b0b2ebf..f39834e89 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -118,7 +118,7 @@ def parse_iso(self, datetime_string, normalize_whitespace=False): num_spaces = datetime_string.count(" ") if has_space_divider and num_spaces != 1 or has_t_divider and num_spaces > 0: raise ParserError( - f"Expected an ISO 8601-like string, but was given '{datetime_string}'. " + f"Expected an ISO 8601-like string, but was given {datetime_string!r}. " "Try passing in a format string to resolve this." ) @@ -228,7 +228,7 @@ def parse(self, datetime_string, fmt, normalize_whitespace=False): if match is None: raise ParserMatchError( - f"Failed to match '{fmt}' when parsing '{datetime_string}'." + f"Failed to match {fmt!r} when parsing {datetime_string!r}." ) parts = {} @@ -242,7 +242,7 @@ def parse(self, datetime_string, fmt, normalize_whitespace=False): if value is None: raise ParserMatchError( - f"Unable to find a match group for the specified token '{token}'." + f"Unable to find a match group for the specified token {token!r}." ) self._parse_token(token, value, parts) @@ -277,7 +277,7 @@ def _generate_pattern_re(self, fmt): try: input_re = self._input_re_map[token] except KeyError: - raise ParserError(f"Unrecognized token '{token}'.") + raise ParserError(f"Unrecognized token {token!r}.") input_pattern = f"(?P<{token}>{input_re.pattern})" tokens.append(token) # a pattern doesn't have the same length as the token @@ -466,7 +466,7 @@ def _build_datetime(parts): dt = datetime.strptime(date_string, "%Y-%j") except ValueError: raise ParserError( - f"The provided day of year '{day_of_year}' is invalid." + f"The provided day of year {day_of_year!r} is invalid." ) parts["year"] = dt.year @@ -552,7 +552,7 @@ def _parse_multiformat(self, string, formats): if _datetime is None: supported_formats = ", ".join(formats) raise ParserError( - f"Could not match input '{string}' to any of the following formats: {supported_formats}." + f"Could not match input {string!r} to any of the following formats: {supported_formats}." ) return _datetime @@ -596,6 +596,6 @@ def parse(cls, tzinfo_string): tzinfo = tz.gettz(tzinfo_string) if tzinfo is None: - raise ParserError(f"Could not parse timezone expression '{tzinfo_string}'.") + raise ParserError(f"Could not parse timezone expression {tzinfo_string!r}.") return tzinfo diff --git a/arrow/util.py b/arrow/util.py index 6e9524594..8c354305e 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -41,9 +41,7 @@ def is_timestamp(value): """Check if value is a valid timestamp.""" if isinstance(value, bool): return False - if not ( - isinstance(value, int) or isinstance(value, float) or isinstance(value, str) - ): + if not isinstance(value, (int, float, str)): return False try: float(value) @@ -68,7 +66,7 @@ def normalize_timestamp(timestamp): elif timestamp < MAX_TIMESTAMP_US: timestamp /= 1e6 else: - raise ValueError(f"The specified timestamp '{timestamp}' is too large.") + raise ValueError(f"The specified timestamp {timestamp!r} is too large.") return timestamp From 6825f8a1b70446571cde6b95c1f57bf8d6d7932c Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Thu, 24 Dec 2020 17:23:22 +0000 Subject: [PATCH 485/649] Remove tzinfo setter and update tests (#902) Co-authored-by: Jad Chaar --- arrow/arrow.py | 6 ------ tests/test_arrow.py | 3 +-- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index b590d6831..31c8b9e60 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -696,12 +696,6 @@ def tzinfo(self): return self._datetime.tzinfo - @tzinfo.setter - def tzinfo(self, tzinfo): - """ Sets the ``tzinfo`` of the :class:`Arrow ` object. """ - - self._datetime = self._datetime.replace(tzinfo=tzinfo) - @property def datetime(self): """Returns a datetime representation of the :class:`Arrow ` object. diff --git a/tests/test_arrow.py b/tests/test_arrow.py index e73e3c2fa..326709ba4 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -285,8 +285,7 @@ def test_getattr_dt_value(self): def test_tzinfo(self): - self.arrow.tzinfo = tz.gettz("PST") - assert self.arrow.tzinfo == tz.gettz("PST") + assert self.arrow.tzinfo == tz.tzutc() def test_naive(self): From 2eba8b97ea323c975fb2400b041f4c17c43c9709 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Mon, 28 Dec 2020 23:49:55 -0500 Subject: [PATCH 486/649] Ignore flake8 rule for pointless comparison in tests; clean up is_between tests. (#905) --- .pre-commit-config.yaml | 4 ++-- requirements.txt | 8 ++++---- tests/test_arrow.py | 34 ++++++++++++++++------------------ 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d456b221b..a84579aee 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: v3.3.0 + rev: v3.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -20,7 +20,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.7.3 + rev: v2.7.4 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/requirements.txt b/requirements.txt index bcf86d346..ce27d0a12 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ dateparser==1.0.* -pre-commit==2.8.* -pytest==6.1.* +pre-commit==2.9.* +pytest==6.2.* pytest-cov==2.10.* -pytest-mock==3.3.* +pytest-mock==3.4.* python-dateutil==2.8.* pytz==2019.* simplejson==3.17.* -sphinx==3.3.* +sphinx==3.4.* diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 326709ba4..f88cf796d 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -355,7 +355,7 @@ def test_gt(self): assert not (self.arrow > self.arrow.datetime) with pytest.raises(TypeError): - self.arrow > "abc" + self.arrow > "abc" # noqa: B015 assert self.arrow < arrow_cmp assert self.arrow < arrow_cmp.datetime @@ -363,7 +363,7 @@ def test_gt(self): def test_ge(self): with pytest.raises(TypeError): - self.arrow >= "abc" + self.arrow >= "abc" # noqa: B015 assert self.arrow >= self.arrow assert self.arrow >= self.arrow.datetime @@ -376,7 +376,7 @@ def test_lt(self): assert not (self.arrow < self.arrow.datetime) with pytest.raises(TypeError): - self.arrow < "abc" + self.arrow < "abc" # noqa: B015 assert self.arrow < arrow_cmp assert self.arrow < arrow_cmp.datetime @@ -384,7 +384,7 @@ def test_lt(self): def test_le(self): with pytest.raises(TypeError): - self.arrow <= "abc" + self.arrow <= "abc" # noqa: B015 assert self.arrow <= self.arrow assert self.arrow <= self.arrow.datetime @@ -2283,45 +2283,43 @@ def test_start_before_end(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 8)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) - result = target.is_between(start, end) - assert not result + assert not target.is_between(start, end) def test_exclusive_exclusive_bounds(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 27)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 10)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 36)) - result = target.is_between(start, end, "()") - assert result - result = target.is_between(start, end) - assert result + assert target.is_between(start, end, "()") def test_exclusive_exclusive_bounds_same_date(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - result = target.is_between(start, end, "()") - assert not result + assert not target.is_between(start, end, "()") def test_inclusive_exclusive_bounds(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 6)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 4)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 6)) - result = target.is_between(start, end, "[)") - assert not result + assert not target.is_between(start, end, "[)") def test_exclusive_inclusive_bounds(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - result = target.is_between(start, end, "(]") - assert result + assert target.is_between(start, end, "(]") def test_inclusive_inclusive_bounds_same_date(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) - result = target.is_between(start, end, "[]") - assert result + assert target.is_between(start, end, "[]") + + def test_inclusive_inclusive_bounds_target_before_start(self): + target = arrow.Arrow.fromdatetime(datetime(2020, 12, 24)) + start = arrow.Arrow.fromdatetime(datetime(2020, 12, 25)) + end = arrow.Arrow.fromdatetime(datetime(2020, 12, 26)) + assert not target.is_between(start, end, "[]") def test_type_error_exception(self): with pytest.raises(TypeError): From 86f2cf319274283802dadc2bad6e367de6cab275 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 2 Jan 2021 16:33:16 -0500 Subject: [PATCH 487/649] Allow test failures on humanize month tests; remove unnecessary date conversion in test_day. (#909) --- tests/test_arrow.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_arrow.py b/tests/test_arrow.py index f88cf796d..2132040ca 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2083,11 +2083,6 @@ def test_day(self): # humanize other argument does not take raw datetime.date objects self.now.humanize(less_than_48_hours_date) - # convert from date to arrow object - less_than_48_hours_date = arrow.Arrow.fromdate(less_than_48_hours_date) - assert self.now.humanize(less_than_48_hours_date) == "a day ago" - assert less_than_48_hours_date.humanize(self.now) == "in a day" - assert self.now.humanize(later, only_distance=True) == "a day" assert later.humanize(self.now, only_distance=True) == "a day" @@ -2151,6 +2146,7 @@ def test_month_plus_4_days(self): 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) From b66baf74172c209e2d038ae0d0cd2a6635d23cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Thu, 7 Jan 2021 18:02:04 +0100 Subject: [PATCH 488/649] Remove redundant Python2 numeric relics (#911) * Remove redundant int conversion (and pyupgrade the whole block) * Remove redundant float conversion --- arrow/arrow.py | 26 +++++++++++++------------- arrow/constants.py | 4 ++-- arrow/formatter.py | 34 +++++++++++++++++----------------- arrow/util.py | 4 ++-- tests/test_parser.py | 4 ++-- 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 31c8b9e60..9433ad976 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -55,12 +55,12 @@ class Arrow: _ATTRS = ["year", "month", "day", "hour", "minute", "second", "microsecond"] _ATTRS_PLURAL = [f"{a}s" for a in _ATTRS] _MONTHS_PER_QUARTER = 3 - _SECS_PER_MINUTE = float(60) - _SECS_PER_HOUR = float(60 * 60) - _SECS_PER_DAY = float(60 * 60 * 24) - _SECS_PER_WEEK = float(60 * 60 * 24 * 7) - _SECS_PER_MONTH = float(60 * 60 * 24 * 30.5) - _SECS_PER_YEAR = float(60 * 60 * 24 * 365) + _SECS_PER_MINUTE = 60 + _SECS_PER_HOUR = 60 * 60 + _SECS_PER_DAY = 60 * 60 * 24 + _SECS_PER_WEEK = 60 * 60 * 24 * 7 + _SECS_PER_MONTH = 60 * 60 * 24 * 30.5 + _SECS_PER_YEAR = 60 * 60 * 24 * 365 def __init__( self, @@ -1028,7 +1028,7 @@ def humanize( if isinstance(granularity, list) and len(granularity) == 1: granularity = granularity[0] - delta = int(round((self._datetime - dt).total_seconds())) + delta = round((self._datetime - dt).total_seconds()) sign = -1 if delta < 0 else 1 diff = abs(delta) delta = diff @@ -1047,7 +1047,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 * int(max(delta / self._SECS_PER_MINUTE, 2)) + minutes = sign * max(delta // self._SECS_PER_MINUTE, 2) return locale.describe( "minutes", minutes, only_distance=only_distance ) @@ -1055,18 +1055,18 @@ 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 * int(max(delta / self._SECS_PER_HOUR, 2)) + hours = sign * max(delta // self._SECS_PER_HOUR, 2) return locale.describe("hours", hours, only_distance=only_distance) elif diff < self._SECS_PER_DAY * 2: return locale.describe("day", sign, only_distance=only_distance) elif diff < self._SECS_PER_WEEK: - days = sign * int(max(delta / self._SECS_PER_DAY, 2)) + days = sign * max(delta // self._SECS_PER_DAY, 2) return locale.describe("days", days, 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 * int(max(delta / self._SECS_PER_WEEK, 2)) + weeks = sign * max(delta // self._SECS_PER_WEEK, 2) return locale.describe("weeks", weeks, only_distance=only_distance) elif diff < self._SECS_PER_MONTH * 2: @@ -1076,7 +1076,7 @@ def humanize( self_months = self._datetime.year * 12 + self._datetime.month other_months = dt.year * 12 + dt.month - months = sign * int(max(abs(other_months - self_months), 2)) + months = sign * max(abs(other_months - self_months), 2) return locale.describe( "months", months, only_distance=only_distance @@ -1085,7 +1085,7 @@ def humanize( elif diff < self._SECS_PER_YEAR * 2: return locale.describe("year", sign, only_distance=only_distance) else: - years = sign * int(max(delta / self._SECS_PER_YEAR, 2)) + years = sign * max(delta // self._SECS_PER_YEAR, 2) return locale.describe("years", years, only_distance=only_distance) elif isinstance(granularity, str): diff --git a/arrow/constants.py b/arrow/constants.py index 830a45f07..afef30f6b 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -20,8 +20,8 @@ else datetime(2038, 1, 18, 23, 59, 59, 999999).timestamp() ) -MAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1e3 -MAX_TIMESTAMP_US = MAX_TIMESTAMP * 1e6 +MAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1000 +MAX_TIMESTAMP_US = MAX_TIMESTAMP * 1_000_000 MAX_ORDINAL = datetime.max.toordinal() MIN_ORDINAL = 1 diff --git a/arrow/formatter.py b/arrow/formatter.py index 0b9ec0cc0..7fe5abbb3 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -51,16 +51,16 @@ def _format_token(self, dt, token): if token == "MM": return f"{dt.month:02d}" if token == "M": - return str(dt.month) + return f"{dt.month}" if token == "DDDD": return f"{dt.timetuple().tm_yday:03d}" if token == "DDD": - return str(dt.timetuple().tm_yday) + return f"{dt.timetuple().tm_yday}" if token == "DD": return f"{dt.day:02d}" if token == "D": - return str(dt.day) + return f"{dt.day}" if token == "Do": return self.locale.ordinal_number(dt.day) @@ -70,45 +70,45 @@ def _format_token(self, dt, token): if token == "ddd": return self.locale.day_abbreviation(dt.isoweekday()) if token == "d": - return str(dt.isoweekday()) + return f"{dt.isoweekday()}" if token == "HH": return f"{dt.hour:02d}" if token == "H": - return str(dt.hour) + return f"{dt.hour}" if token == "hh": - return "{:02d}".format(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) + return f"{dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12):02d}" if token == "h": - return str(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) + return f"{dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)}" if token == "mm": return f"{dt.minute:02d}" if token == "m": - return str(dt.minute) + return f"{dt.minute}" if token == "ss": return f"{dt.second:02d}" if token == "s": - return str(dt.second) + return f"{dt.second}" if token == "SSSSSS": - return str("{:06d}".format(int(dt.microsecond))) + return f"{dt.microsecond:06d}" if token == "SSSSS": - return str("{:05d}".format(int(dt.microsecond / 10))) + return f"{dt.microsecond // 10:05d}" if token == "SSSS": - return str("{:04d}".format(int(dt.microsecond / 100))) + return f"{dt.microsecond // 100:04d}" if token == "SSS": - return str("{:03d}".format(int(dt.microsecond / 1000))) + return f"{dt.microsecond // 1000:03d}" if token == "SS": - return str("{:02d}".format(int(dt.microsecond / 10000))) + return f"{dt.microsecond // 10000:02d}" if token == "S": - return str(int(dt.microsecond / 100000)) + return f"{dt.microsecond // 100000}" if token == "X": - return str(dt.timestamp()) + return f"{dt.timestamp()}" if token == "x": - return str(int(dt.timestamp() * 1000000)) + return f"{dt.timestamp() * 1_000_000:.0f}" if token == "ZZZ": return dt.tzname() diff --git a/arrow/util.py b/arrow/util.py index 8c354305e..39381be5e 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -62,9 +62,9 @@ def normalize_timestamp(timestamp): """Normalize millisecond and microsecond timestamps into normal timestamps.""" if timestamp > MAX_TIMESTAMP: if timestamp < MAX_TIMESTAMP_MS: - timestamp /= 1e3 + timestamp /= 1000 elif timestamp < MAX_TIMESTAMP_US: - timestamp /= 1e6 + timestamp /= 1_000_000 else: raise ValueError(f"The specified timestamp {timestamp!r} is too large.") return timestamp diff --git a/tests/test_parser.py b/tests/test_parser.py index 69ff4b6fc..bdfb30614 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -275,8 +275,8 @@ def test_parse_expanded_timestamp(self): tz_utc = tz.tzutc() timestamp = 1569982581.413132 - timestamp_milli = int(round(timestamp * 1e3)) - timestamp_micro = int(round(timestamp * 1e6)) + timestamp_milli = round(timestamp * 1000) + timestamp_micro = round(timestamp * 1_000_000) # "x" token should parse integer timestamps below MAX_TIMESTAMP normally self.expected = datetime.fromtimestamp(int(timestamp), tz=tz_utc) From fb6913b5038b65b2da496cb8d7ac5b51c54961dc Mon Sep 17 00:00:00 2001 From: Armand Tavera Date: Thu, 7 Jan 2021 11:20:42 -0600 Subject: [PATCH 489/649] Raise exception when None value is passed to arrow.get() (#906) * raised NameException on None value for arrow.get * Refactored arrow.get(None) test to check for a NameException * Refactored test to use unittest function to assertException * Changed NameError to ValueError on arrow.get(None) and asserting the raise of error using pytest instead of unittest, removed traces of unittest * Changed ValueError to TypeError with description, removed unnecessary comment, updated test to expect typeerror * Update message Co-authored-by: Jad Chaar Co-authored-by: Jad Chaar --- arrow/factory.py | 4 ++-- tests/test_factory.py | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/arrow/factory.py b/arrow/factory.py index b25170498..9aa44b8ee 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -165,9 +165,9 @@ def get(self, *args, **kwargs): if arg_count == 1: arg = args[0] - # (None) -> now, @ utc. + # (None) -> raises an exception if arg is None: - return self.type.utcnow() + raise TypeError("Cannot parse argument of type None.") # try (int, float) -> from timestamp with tz elif not isinstance(arg, str) and is_timestamp(arg): diff --git a/tests/test_factory.py b/tests/test_factory.py index b954d1620..5e0020d65 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -25,10 +25,8 @@ def test_timestamp_one_arg_no_arg(self): assert no_arg == one_arg def test_one_arg_none(self): - - assert_datetime_equality( - self.factory.get(None), datetime.utcnow().replace(tzinfo=tz.tzutc()) - ) + with pytest.raises(TypeError): + self.factory.get(None) def test_struct_time(self): From f2e08490fb86719c83000056353861f4b5a0a562 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen <4123478+tido64@users.noreply.github.com> Date: Tue, 2 Feb 2021 01:33:47 +0100 Subject: [PATCH 490/649] Correct Norwegian grammar (#915) --- arrow/locales.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 3cad6b07e..ca3666a3e 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1853,8 +1853,8 @@ class NorwegianLocale(Locale): timeframes = { "now": "nå nettopp", - "second": "et sekund", - "seconds": "{0} noen sekunder", + "second": "ett sekund", + "seconds": "{0} sekunder", "minute": "ett minutt", "minutes": "{0} minutter", "hour": "en time", @@ -1920,9 +1920,9 @@ class NewNorwegianLocale(Locale): timeframes = { "now": "no nettopp", - "second": "et sekund", - "seconds": "{0} nokre sekund", - "minute": "ett minutt", + "second": "eitt sekund", + "seconds": "{0} sekund", + "minute": "eitt minutt", "minutes": "{0} minutt", "hour": "ein time", "hours": "{0} timar", @@ -1930,7 +1930,7 @@ class NewNorwegianLocale(Locale): "days": "{0} dagar", "month": "en månad", "months": "{0} månader", - "year": "eit år", + "year": "eitt år", "years": "{0} år", } From 53029db7abf4f83e5be12a4e4651b3068de52bf2 Mon Sep 17 00:00:00 2001 From: Isac Byeonghoon Yoo Date: Thu, 18 Feb 2021 03:42:35 +0900 Subject: [PATCH 491/649] Add type annotations (#883) * Add typing_extentions as dependency * Add mypy pre-commit hook * Add mypy configuration file * Ignore unnecessary mypy errors * Add type annotation * Add TODO comment to api.get() * Handle NotImplemented in arithmetic operators * Remove ABCMeta * Fix Final * Apply Final to formatter.py * Handle when tz.utcoffset() is None * Apply Final to constants.py * Remove TODO comment * Fix setup.py * Fix typos * Narrowing type for timeframes * Fix unit test coverage * Fix unit test coverage * Do not use SupportsFloat yet * Use loop * Refactor Arrow.humanize * Remove redundant Union * Revert "Handle when tz.utcoffset() is None" * Do not accept None on Factory.get * Make type error explicit * Fix return type of Arrow.tzinfo * Add warning comment for naive object handling * Rename TimeFrames to TimeFrameLiteral * Use final on ordinal constants * Use explicit typing on parser.py * Use explicit typing on parser.py * Use explicit typing * Fix signature of Arrow.fromdatetime * Remove redundant magic method overloads Co-authored-by: Jad Chaar --- .pre-commit-config.yaml | 4 + arrow/api.py | 73 +++++- arrow/arrow.py | 480 +++++++++++++++++++++++++--------------- arrow/constants.py | 18 +- arrow/factory.py | 72 +++++- arrow/formatter.py | 49 ++-- arrow/locales.py | 347 ++++++++++++++++++++--------- arrow/parser.py | 278 ++++++++++++++++++----- arrow/util.py | 20 +- docs/conf.py | 1 + setup.cfg | 34 +++ setup.py | 6 +- 12 files changed, 999 insertions(+), 383 deletions(-) create mode 100644 setup.cfg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a84579aee..25a7fb0ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,3 +40,7 @@ repos: hooks: - id: flake8 additional_dependencies: [flake8-bugbear] + - repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v0.790' + hooks: + - id: mypy diff --git a/arrow/api.py b/arrow/api.py index 13a369f82..95696f3c1 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -4,14 +4,77 @@ """ +from datetime import date, datetime +from datetime import tzinfo as dt_tzinfo +from time import struct_time +from typing import Any, List, Optional, Tuple, Type, Union, overload +from arrow.arrow import TZ_EXPR, Arrow from arrow.factory import ArrowFactory # internal default factory. _factory = ArrowFactory() - -def get(*args, **kwargs): +# TODO: Use Positional Only Argument (https://www.python.org/dev/peps/pep-0570/) +# after Python 3.7 deprecation + + +@overload +def get( + *, + locale: str = "en_us", + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, +) -> Arrow: + ... # pragma: no cover + + +@overload +def get( + __obj: Union[ + Arrow, + datetime, + date, + struct_time, + dt_tzinfo, + int, + float, + str, + Tuple[int, int, int], + ], + *, + locale: str = "en_us", + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, +) -> Arrow: + ... # pragma: no cover + + +@overload +def get( + __arg1: Union[datetime, date], + __arg2: TZ_EXPR, + *, + locale: str = "en_us", + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, +) -> Arrow: + ... # pragma: no cover + + +@overload +def get( + __arg1: str, + __arg2: Union[str, List[str]], + *, + locale: str = "en_us", + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, +) -> Arrow: + ... # pragma: no cover + + +def get(*args: Any, **kwargs: Any) -> Arrow: """Calls the default :class:`ArrowFactory ` ``get`` method.""" return _factory.get(*args, **kwargs) @@ -20,7 +83,7 @@ def get(*args, **kwargs): get.__doc__ = _factory.get.__doc__ -def utcnow(): +def utcnow() -> Arrow: """Calls the default :class:`ArrowFactory ` ``utcnow`` method.""" return _factory.utcnow() @@ -29,7 +92,7 @@ def utcnow(): utcnow.__doc__ = _factory.utcnow.__doc__ -def now(tz=None): +def now(tz: Optional[TZ_EXPR] = None) -> Arrow: """Calls the default :class:`ArrowFactory ` ``now`` method.""" return _factory.now(tz) @@ -38,7 +101,7 @@ def now(tz=None): now.__doc__ = _factory.now.__doc__ -def factory(type): +def factory(type: Type[Arrow]) -> ArrowFactory: """Returns an :class:`.ArrowFactory` for the specified :class:`Arrow ` or derived type. diff --git a/arrow/arrow.py b/arrow/arrow.py index 9433ad976..f2a2d27c2 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -7,14 +7,74 @@ import calendar import sys -from datetime import datetime, timedelta +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 tzinfo as dt_tzinfo from math import trunc +from time import struct_time +from typing import ( + Any, + ClassVar, + Generator, + Iterable, + List, + Mapping, + Optional, + Tuple, + Union, + cast, + overload, +) from dateutil import tz as dateutil_tz from dateutil.relativedelta import relativedelta from arrow import formatter, locales, parser, util +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[ + "year", + "years", + "month", + "months", + "day", + "days", + "hour", + "hours", + "minute", + "minutes", + "second", + "seconds", + "microsecond", + "microseconds", + "week", + "weeks", + "quarter", + "quarters", +] + +_BOUNDS = Literal["[)", "()", "(]", "[]"] + +_GRANULARITY = Literal[ + "auto", + "second", + "minute", + "hour", + "day", + "week", + "month", + "year", +] class Arrow: @@ -50,30 +110,52 @@ class Arrow: """ - resolution = datetime.resolution - - _ATTRS = ["year", "month", "day", "hour", "minute", "second", "microsecond"] - _ATTRS_PLURAL = [f"{a}s" for a in _ATTRS] - _MONTHS_PER_QUARTER = 3 - _SECS_PER_MINUTE = 60 - _SECS_PER_HOUR = 60 * 60 - _SECS_PER_DAY = 60 * 60 * 24 - _SECS_PER_WEEK = 60 * 60 * 24 * 7 - _SECS_PER_MONTH = 60 * 60 * 24 * 30.5 - _SECS_PER_YEAR = 60 * 60 * 24 * 365 + resolution: ClassVar[timedelta] = dt_datetime.resolution + min: ClassVar["Arrow"] + max: ClassVar["Arrow"] + + _ATTRS: Final[List[str]] = [ + "year", + "month", + "day", + "hour", + "minute", + "second", + "microsecond", + ] + _ATTRS_PLURAL: Final[List[str]] = [f"{a}s" for a in _ATTRS] + _MONTHS_PER_QUARTER: Final[int] = 3 + _SECS_PER_MINUTE: Final[int] = 60 + _SECS_PER_HOUR: Final[int] = 60 * 60 + _SECS_PER_DAY: Final[int] = 60 * 60 * 24 + _SECS_PER_WEEK: Final[int] = 60 * 60 * 24 * 7 + _SECS_PER_MONTH: Final[float] = 60 * 60 * 24 * 30.5 + _SECS_PER_YEAR: Final[int] = 60 * 60 * 24 * 365 + + _SECS_MAP: Final[Mapping[TimeFrameLiteral, float]] = { + "second": 1.0, + "minute": _SECS_PER_MINUTE, + "hour": _SECS_PER_HOUR, + "day": _SECS_PER_DAY, + "week": _SECS_PER_WEEK, + "month": _SECS_PER_MONTH, + "year": _SECS_PER_YEAR, + } + + _datetime: dt_datetime def __init__( self, - year, - month, - day, - hour=0, - minute=0, - second=0, - microsecond=0, - tzinfo=None, - **kwargs, - ): + year: int, + month: int, + day: int, + hour: int = 0, + minute: int = 0, + second: int = 0, + microsecond: int = 0, + tzinfo: Optional[TZ_EXPR] = None, + **kwargs: Any, + ) -> None: if tzinfo is None: tzinfo = dateutil_tz.tzutc() # detect that tzinfo is a pytz object (issue #626) @@ -81,24 +163,24 @@ def __init__( isinstance(tzinfo, dt_tzinfo) and hasattr(tzinfo, "localize") and hasattr(tzinfo, "zone") - and tzinfo.zone + and tzinfo.zone # type: ignore[attr-defined] ): - tzinfo = parser.TzinfoParser.parse(tzinfo.zone) + tzinfo = parser.TzinfoParser.parse(tzinfo.zone) # type: ignore[attr-defined] elif isinstance(tzinfo, str): tzinfo = parser.TzinfoParser.parse(tzinfo) fold = kwargs.get("fold", 0) # use enfold here to cover direct arrow.Arrow init on 2.7/3.5 - self._datetime = dateutil_tz.enfold( - datetime(year, month, day, hour, minute, second, microsecond, tzinfo), + self._datetime = dateutil_tz.enfold( # type: ignore + dt_datetime(year, month, day, hour, minute, second, microsecond, tzinfo), fold=fold, ) # factories: single object, both original and from datetime. @classmethod - def now(cls, tzinfo=None): + def now(cls, tzinfo: Optional[dt_tzinfo] = None) -> "Arrow": """Constructs an :class:`Arrow ` object, representing "now" in the given timezone. @@ -114,7 +196,7 @@ def now(cls, tzinfo=None): if tzinfo is None: tzinfo = dateutil_tz.tzlocal() - dt = datetime.now(tzinfo) + dt = dt_datetime.now(tzinfo) return cls( dt.year, @@ -129,7 +211,7 @@ def now(cls, tzinfo=None): ) @classmethod - def utcnow(cls): + def utcnow(cls) -> "Arrow": """Constructs an :class:`Arrow ` object, representing "now" in UTC time. @@ -140,7 +222,7 @@ def utcnow(cls): """ - dt = datetime.now(dateutil_tz.tzutc()) + dt = dt_datetime.now(dateutil_tz.tzutc()) return cls( dt.year, @@ -155,7 +237,11 @@ def utcnow(cls): ) @classmethod - def fromtimestamp(cls, timestamp, tzinfo=None): + def fromtimestamp( + cls, + timestamp: Union[int, float, str], + tzinfo: Optional[TZ_EXPR] = None, + ) -> "Arrow": """Constructs an :class:`Arrow ` object from a timestamp, converted to the given timezone. @@ -172,7 +258,7 @@ def fromtimestamp(cls, timestamp, tzinfo=None): raise ValueError(f"The provided timestamp {timestamp!r} is invalid.") timestamp = util.normalize_timestamp(float(timestamp)) - dt = datetime.fromtimestamp(timestamp, tzinfo) + dt = dt_datetime.fromtimestamp(timestamp, tzinfo) return cls( dt.year, @@ -187,7 +273,7 @@ def fromtimestamp(cls, timestamp, tzinfo=None): ) @classmethod - def utcfromtimestamp(cls, timestamp): + def utcfromtimestamp(cls, timestamp: Union[int, float, str]) -> "Arrow": """Constructs an :class:`Arrow ` object from a timestamp, in UTC time. :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. @@ -198,7 +284,7 @@ def utcfromtimestamp(cls, timestamp): raise ValueError(f"The provided timestamp {timestamp!r} is invalid.") timestamp = util.normalize_timestamp(float(timestamp)) - dt = datetime.utcfromtimestamp(timestamp) + dt = dt_datetime.utcfromtimestamp(timestamp) return cls( dt.year, @@ -213,7 +299,7 @@ def utcfromtimestamp(cls, timestamp): ) @classmethod - def fromdatetime(cls, dt, tzinfo=None): + def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": """Constructs an :class:`Arrow ` object from a ``datetime`` and optional replacement timezone. @@ -249,7 +335,7 @@ def fromdatetime(cls, dt, tzinfo=None): ) @classmethod - def fromdate(cls, date, tzinfo=None): + def fromdate(cls, date: date, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": """Constructs an :class:`Arrow ` object from a ``date`` and optional replacement timezone. Time values are set to 0. @@ -263,7 +349,9 @@ def fromdate(cls, date, tzinfo=None): return cls(date.year, date.month, date.day, tzinfo=tzinfo) @classmethod - def strptime(cls, date_str, fmt, tzinfo=None): + def strptime( + cls, date_str: str, fmt: str, tzinfo: Optional[TZ_EXPR] = None + ) -> "Arrow": """Constructs an :class:`Arrow ` object from a date string and format, in the style of ``datetime.strptime``. Optionally replaces the parsed timezone. @@ -279,7 +367,7 @@ def strptime(cls, date_str, fmt, tzinfo=None): """ - dt = datetime.strptime(date_str, fmt) + dt = dt_datetime.strptime(date_str, fmt) if tzinfo is None: tzinfo = dt.tzinfo @@ -296,7 +384,7 @@ def strptime(cls, date_str, fmt, tzinfo=None): ) @classmethod - def fromordinal(cls, ordinal): + def fromordinal(cls, ordinal: int) -> "Arrow": """Constructs an :class:`Arrow ` object corresponding to the Gregorian Ordinal. @@ -310,7 +398,7 @@ def fromordinal(cls, ordinal): """ util.validate_ordinal(ordinal) - dt = datetime.fromordinal(ordinal) + dt = dt_datetime.fromordinal(ordinal) return cls( dt.year, dt.month, @@ -326,7 +414,14 @@ def fromordinal(cls, ordinal): # factories: ranges and spans @classmethod - def range(cls, frame, start, end=None, tz=None, limit=None): + def range( + cls, + frame: _T_FRAMES, + start: Union["Arrow", dt_datetime], + end: Union["Arrow", dt_datetime, None] = None, + tz: Optional[TZ_EXPR] = None, + limit: Optional[int] = None, + ) -> Generator["Arrow", None, None]: """Returns an iterator of :class:`Arrow ` objects, representing points in time between two inputs. @@ -395,7 +490,7 @@ def range(cls, frame, start, end=None, tz=None, limit=None): yield current values = [getattr(current, f) for f in cls._ATTRS] - current = cls(*values, tzinfo=tzinfo).shift( + current = cls(*values, tzinfo=tzinfo).shift( # type: ignore **{frame_relative: relative_steps} ) @@ -405,7 +500,13 @@ def range(cls, frame, start, end=None, tz=None, limit=None): if day_is_clipped and not cls._is_last_day_of_month(current): current = current.replace(day=original_day) - def span(self, frame, count=1, bounds="[)", exact=False): + def span( + self, + frame: _T_FRAMES, + count: int = 1, + bounds: _BOUNDS = "[)", + exact: bool = False, + ) -> Tuple["Arrow", "Arrow"]: """Returns two new :class:`Arrow ` objects, representing the timespan of the :class:`Arrow ` object in a given timeframe. @@ -461,7 +562,7 @@ def span(self, frame, count=1, bounds="[)", exact=False): for _ in range(3 - len(values)): values.append(1) - floor = self.__class__(*values, tzinfo=self.tzinfo) + floor = self.__class__(*values, tzinfo=self.tzinfo) # type: ignore if frame_absolute == "week": floor = floor.shift(days=-(self.isoweekday() - 1)) @@ -478,7 +579,7 @@ def span(self, frame, count=1, bounds="[)", exact=False): return floor, ceil - def floor(self, frame): + def floor(self, frame: _T_FRAMES) -> "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 @@ -494,7 +595,7 @@ def floor(self, frame): return self.span(frame)[0] - def ceil(self, frame): + def ceil(self, frame: _T_FRAMES) -> "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 @@ -512,8 +613,15 @@ def ceil(self, frame): @classmethod def span_range( - cls, frame, start, end, tz=None, limit=None, bounds="[)", exact=False - ): + cls, + frame: _T_FRAMES, + start: dt_datetime, + end: dt_datetime, + tz: Optional[TZ_EXPR] = None, + limit: Optional[int] = None, + bounds: _BOUNDS = "[)", + exact: bool = False, + ) -> Iterable[Tuple["Arrow", "Arrow"]]: """Returns an iterator of tuples, each :class:`Arrow ` objects, representing a series of timespans between two inputs. @@ -586,7 +694,16 @@ def span_range( yield floor, ceil @classmethod - def interval(cls, frame, start, end, interval=1, tz=None, bounds="[)", exact=False): + def interval( + cls, + frame: _T_FRAMES, + start: dt_datetime, + end: dt_datetime, + interval: int = 1, + tz: Optional[TZ_EXPR] = None, + bounds: _BOUNDS = "[)", + exact: bool = False, + ) -> Iterable[Tuple["Arrow", "Arrow"]]: """Returns an iterator of tuples, each :class:`Arrow ` objects, representing a series of intervals between two inputs. @@ -648,25 +765,25 @@ def interval(cls, frame, start, end, interval=1, tz=None, bounds="[)", exact=Fal # representations - def __repr__(self): + def __repr__(self) -> str: return f"<{self.__class__.__name__} [{self.__str__()}]>" - def __str__(self): + def __str__(self) -> str: return self._datetime.isoformat() - def __format__(self, formatstr): + def __format__(self, formatstr: str) -> str: if len(formatstr) > 0: return self.format(formatstr) return str(self) - def __hash__(self): + def __hash__(self) -> int: return self._datetime.__hash__() # attributes and properties - def __getattr__(self, name): + def __getattr__(self, name: str) -> int: if name == "week": return self.isocalendar()[1] @@ -675,15 +792,15 @@ def __getattr__(self, name): return int((self.month - 1) / self._MONTHS_PER_QUARTER) + 1 if not name.startswith("_"): - value = getattr(self._datetime, name, None) + value: Optional[int] = getattr(self._datetime, name, None) if value is not None: return value - return object.__getattribute__(self, name) + return cast(int, object.__getattribute__(self, name)) @property - def tzinfo(self): + def tzinfo(self) -> dt_tzinfo: """Gets the ``tzinfo`` of the :class:`Arrow ` object. Usage:: @@ -694,10 +811,11 @@ def tzinfo(self): """ - return self._datetime.tzinfo + # In Arrow, `_datetime` cannot be naive. + return cast(dt_tzinfo, self._datetime.tzinfo) @property - def datetime(self): + def datetime(self) -> dt_datetime: """Returns a datetime representation of the :class:`Arrow ` object. Usage:: @@ -711,7 +829,7 @@ def datetime(self): return self._datetime @property - def naive(self): + def naive(self) -> dt_datetime: """Returns a naive datetime representation of the :class:`Arrow ` object. @@ -727,7 +845,7 @@ def naive(self): return self._datetime.replace(tzinfo=None) - def timestamp(self): + def timestamp(self) -> float: """Returns a timestamp representation of the :class:`Arrow ` object, in UTC time. @@ -741,7 +859,7 @@ def timestamp(self): return self._datetime.timestamp() @property - def int_timestamp(self): + def int_timestamp(self) -> int: """Returns a timestamp representation of the :class:`Arrow ` object, in UTC time. @@ -755,7 +873,7 @@ def int_timestamp(self): return int(self.timestamp()) @property - def float_timestamp(self): + def float_timestamp(self) -> float: """Returns a floating-point representation of the :class:`Arrow ` object, in UTC time. @@ -769,28 +887,26 @@ def float_timestamp(self): return self.timestamp() @property - def fold(self): + def fold(self) -> int: """ Returns the ``fold`` value of the :class:`Arrow ` object. """ - # in python < 3.6 _datetime will be a _DatetimeWithFold if fold=1 and a datetime with no fold attribute - # otherwise, so we need to return zero to cover the latter case - return getattr(self._datetime, "fold", 0) + return self._datetime.fold @property - def ambiguous(self): + def ambiguous(self) -> bool: """ Returns a boolean indicating whether the :class:`Arrow ` object is ambiguous.""" return dateutil_tz.datetime_ambiguous(self._datetime) @property - def imaginary(self): + def imaginary(self) -> bool: """Indicates whether the :class: `Arrow ` object exists in the current timezone.""" return not dateutil_tz.datetime_exists(self._datetime) # mutation and duplication. - def clone(self): + def clone(self) -> "Arrow": """Returns a new :class:`Arrow ` object, cloned from the current one. Usage: @@ -802,7 +918,7 @@ def clone(self): return self.fromdatetime(self._datetime) - def replace(self, **kwargs): + def replace(self, **kwargs: Any) -> "Arrow": """Returns a new :class:`Arrow ` object with attributes updated according to inputs. @@ -846,11 +962,11 @@ def replace(self, **kwargs): # TODO revisit this once we drop support for 2.7/3.5 if fold is not None: - current = dateutil_tz.enfold(current, fold=fold) + current = dateutil_tz.enfold(current, fold=fold) # type: ignore return self.fromdatetime(current) - def shift(self, **kwargs): + def shift(self, **kwargs: Any) -> "Arrow": """Returns a new :class:`Arrow ` object with attributes updated according to inputs. @@ -906,7 +1022,7 @@ def shift(self, **kwargs): return self.fromdatetime(current) - def to(self, tz): + def to(self, tz: TZ_EXPR) -> "Arrow": """Returns a new :class:`Arrow ` object, converted to the target timezone. @@ -954,7 +1070,7 @@ def to(self, tz): # string output and formatting - def format(self, fmt="YYYY-MM-DD HH:mm:ssZZ", locale="en_us"): + def format(self, fmt: str = "YYYY-MM-DD HH:mm:ssZZ", locale: str = "en_us") -> str: """Returns a string representation of the :class:`Arrow ` object, formatted according to a format string. @@ -980,8 +1096,12 @@ def format(self, fmt="YYYY-MM-DD HH:mm:ssZZ", locale="en_us"): return formatter.DateTimeFormatter(locale).format(self._datetime, fmt) def humanize( - self, other=None, locale="en_us", only_distance=False, granularity="auto" - ): + self, + other: Union["Arrow", dt_datetime, None] = None, + locale: str = "en_us", + only_distance: bool = False, + granularity: Union[_GRANULARITY, List[_GRANULARITY]] = "auto", + ) -> str: """Returns a localized, humanized representation of a relative difference in time. :param other: (optional) an :class:`Arrow ` or ``datetime`` object. @@ -1007,13 +1127,13 @@ def humanize( locale = locales.get_locale(locale) if other is None: - utc = datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc()) + utc = dt_datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc()) dt = utc.astimezone(self._datetime.tzinfo) elif isinstance(other, Arrow): dt = other._datetime - elif isinstance(other, datetime): + elif isinstance(other, dt_datetime): if other.tzinfo is None: dt = other.replace(tzinfo=self._datetime.tzinfo) else: @@ -1028,10 +1148,9 @@ def humanize( if isinstance(granularity, list) and len(granularity) == 1: granularity = granularity[0] - delta = round((self._datetime - dt).total_seconds()) - sign = -1 if delta < 0 else 1 - diff = abs(delta) - delta = diff + _delta = int(round((self._datetime - dt).total_seconds())) + sign = -1 if _delta < 0 else 1 + delta_second = diff = abs(_delta) try: if granularity == "auto": @@ -1039,7 +1158,7 @@ def humanize( return locale.describe("now", only_distance=only_distance) if diff < self._SECS_PER_MINUTE: - seconds = sign * delta + seconds = sign * delta_second return locale.describe( "seconds", seconds, only_distance=only_distance ) @@ -1047,7 +1166,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 // self._SECS_PER_MINUTE, 2) + minutes = sign * max(delta_second // self._SECS_PER_MINUTE, 2) return locale.describe( "minutes", minutes, only_distance=only_distance ) @@ -1055,18 +1174,18 @@ 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 // self._SECS_PER_HOUR, 2) + 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: return locale.describe("day", sign, only_distance=only_distance) elif diff < self._SECS_PER_WEEK: - days = sign * max(delta // self._SECS_PER_DAY, 2) + 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: return locale.describe("week", sign, only_distance=only_distance) elif diff < self._SECS_PER_MONTH: - weeks = sign * max(delta // self._SECS_PER_WEEK, 2) + 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: @@ -1085,26 +1204,28 @@ def humanize( elif diff < self._SECS_PER_YEAR * 2: return locale.describe("year", sign, only_distance=only_distance) else: - years = sign * max(delta // self._SECS_PER_YEAR, 2) + years = sign * max(delta_second // self._SECS_PER_YEAR, 2) return locale.describe("years", years, only_distance=only_distance) elif isinstance(granularity, str): + granularity = cast(TimeFrameLiteral, granularity) # type: ignore[assignment] + if granularity == "second": - delta = sign * delta + delta = sign * float(delta_second) if abs(delta) < 2: return locale.describe("now", only_distance=only_distance) elif granularity == "minute": - delta = sign * delta / self._SECS_PER_MINUTE + delta = sign * delta_second / self._SECS_PER_MINUTE elif granularity == "hour": - delta = sign * delta / self._SECS_PER_HOUR + delta = sign * delta_second / self._SECS_PER_HOUR elif granularity == "day": - delta = sign * delta / self._SECS_PER_DAY + delta = sign * delta_second / self._SECS_PER_DAY elif granularity == "week": - delta = sign * delta / self._SECS_PER_WEEK + delta = sign * delta_second / self._SECS_PER_WEEK elif granularity == "month": - delta = sign * delta / self._SECS_PER_MONTH + delta = sign * delta_second / self._SECS_PER_MONTH elif granularity == "year": - delta = sign * delta / self._SECS_PER_YEAR + delta = sign * delta_second / self._SECS_PER_YEAR else: raise ValueError( "Invalid level of granularity. " @@ -1112,44 +1233,36 @@ def humanize( ) if trunc(abs(delta)) != 1: - granularity += "s" + granularity += "s" # type: ignore return locale.describe(granularity, delta, only_distance=only_distance) else: - timeframes = [] - if "year" in granularity: - years = sign * delta / self._SECS_PER_YEAR - delta %= self._SECS_PER_YEAR - timeframes.append(["year", years]) - - if "month" in granularity: - months = sign * delta / self._SECS_PER_MONTH - delta %= self._SECS_PER_MONTH - timeframes.append(["month", months]) - - if "week" in granularity: - weeks = sign * delta / self._SECS_PER_WEEK - delta %= self._SECS_PER_WEEK - timeframes.append(["week", weeks]) - - if "day" in granularity: - days = sign * delta / self._SECS_PER_DAY - delta %= self._SECS_PER_DAY - timeframes.append(["day", days]) - - if "hour" in granularity: - hours = sign * delta / self._SECS_PER_HOUR - delta %= self._SECS_PER_HOUR - timeframes.append(["hour", hours]) - - if "minute" in granularity: - minutes = sign * delta / self._SECS_PER_MINUTE - delta %= self._SECS_PER_MINUTE - timeframes.append(["minute", minutes]) - - if "second" in granularity: - seconds = sign * delta - timeframes.append(["second", seconds]) + timeframes: List[Tuple[TimeFrameLiteral, float]] = [] + + def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float: + if _frame in granularity: + value = sign * _delta / self._SECS_MAP[_frame] + _delta %= self._SECS_MAP[_frame] + if trunc(abs(value)) != 1: + timeframes.append( + (cast(TimeFrameLiteral, _frame + "s"), value) + ) + else: + timeframes.append((_frame, value)) + return _delta + + delta = float(delta_second) + frames: Tuple[TimeFrameLiteral, ...] = ( + "year", + "month", + "week", + "day", + "hour", + "minute", + "second", + ) + for frame in frames: + delta = gather_timeframes(delta, frame) if len(timeframes) < len(granularity): raise ValueError( @@ -1157,10 +1270,6 @@ def humanize( "Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'." ) - for tf in timeframes: - # Make granularity plural if the delta is not equal to 1 - if trunc(abs(tf[1])) != 1: - tf[0] += "s" return locale.describe_multi(timeframes, only_distance=only_distance) except KeyError as e: @@ -1171,7 +1280,12 @@ def humanize( # query functions - def is_between(self, start, end, bounds="()"): + def is_between( + self, + start: "Arrow", + end: "Arrow", + bounds: _BOUNDS = "()", + ) -> bool: """Returns a boolean denoting whether the specified date and time is between the start and end dates and times. @@ -1226,7 +1340,7 @@ def is_between(self, start, end, bounds="()"): # datetime methods - def date(self): + def date(self) -> date: """Returns a ``date`` object with the same year, month and day. Usage:: @@ -1238,7 +1352,7 @@ def date(self): return self._datetime.date() - def time(self): + def time(self) -> dt_time: """Returns a ``time`` object with the same hour, minute, second, microsecond. Usage:: @@ -1250,7 +1364,7 @@ def time(self): return self._datetime.time() - def timetz(self): + def timetz(self) -> dt_time: """Returns a ``time`` object with the same hour, minute, second, microsecond and tzinfo. @@ -1263,7 +1377,7 @@ def timetz(self): return self._datetime.timetz() - def astimezone(self, tz): + def astimezone(self, tz: Optional[dt_tzinfo]) -> dt_datetime: """Returns a ``datetime`` object, converted to the specified timezone. :param tz: a ``tzinfo`` object. @@ -1279,7 +1393,7 @@ def astimezone(self, tz): return self._datetime.astimezone(tz) - def utcoffset(self): + def utcoffset(self) -> Optional[timedelta]: """Returns a ``timedelta`` object representing the whole number of minutes difference from UTC time. @@ -1292,7 +1406,7 @@ def utcoffset(self): return self._datetime.utcoffset() - def dst(self): + def dst(self) -> Optional[timedelta]: """Returns the daylight savings time adjustment. Usage:: @@ -1304,7 +1418,7 @@ def dst(self): return self._datetime.dst() - def timetuple(self): + def timetuple(self) -> struct_time: """Returns a ``time.struct_time``, in the current timezone. Usage:: @@ -1316,7 +1430,7 @@ def timetuple(self): return self._datetime.timetuple() - def utctimetuple(self): + def utctimetuple(self) -> struct_time: """Returns a ``time.struct_time``, in UTC time. Usage:: @@ -1328,7 +1442,7 @@ def utctimetuple(self): return self._datetime.utctimetuple() - def toordinal(self): + def toordinal(self) -> int: """Returns the proleptic Gregorian ordinal of the date. Usage:: @@ -1340,7 +1454,7 @@ def toordinal(self): return self._datetime.toordinal() - def weekday(self): + def weekday(self) -> int: """Returns the day of the week as an integer (0-6). Usage:: @@ -1352,7 +1466,7 @@ def weekday(self): return self._datetime.weekday() - def isoweekday(self): + def isoweekday(self) -> int: """Returns the ISO day of the week as an integer (1-7). Usage:: @@ -1364,7 +1478,7 @@ def isoweekday(self): return self._datetime.isoweekday() - def isocalendar(self): + def isocalendar(self) -> Tuple[int, int, int]: """Returns a 3-tuple, (ISO year, ISO week number, ISO weekday). Usage:: @@ -1376,7 +1490,7 @@ def isocalendar(self): return self._datetime.isocalendar() - def isoformat(self, sep="T", timespec="auto"): + def isoformat(self, sep: str = "T", timespec: str = "auto") -> str: """Returns an ISO 8601 formatted representation of the date and time. Usage:: @@ -1388,7 +1502,7 @@ def isoformat(self, sep="T", timespec="auto"): return self._datetime.isoformat(sep, timespec) - def ctime(self): + def ctime(self) -> str: """Returns a ctime formatted representation of the date and time. Usage:: @@ -1400,7 +1514,7 @@ def ctime(self): return self._datetime.ctime() - def strftime(self, format): + def strftime(self, format: str) -> str: """Formats in the style of ``datetime.strftime``. :param format: the format string. @@ -1414,7 +1528,7 @@ def strftime(self, format): return self._datetime.strftime(format) - def for_json(self): + def for_json(self) -> str: """Serializes for the ``for_json`` protocol of simplejson. Usage:: @@ -1428,22 +1542,30 @@ def for_json(self): # math - def __add__(self, other): + def __add__(self, other: Any) -> "Arrow": if isinstance(other, (timedelta, relativedelta)): return self.fromdatetime(self._datetime + other, self._datetime.tzinfo) return NotImplemented - def __radd__(self, other): + def __radd__(self, other: Union[timedelta, relativedelta]) -> "Arrow": return self.__add__(other) - def __sub__(self, other): + @overload + def __sub__(self, other: Union[timedelta, relativedelta]) -> "Arrow": + pass # pragma: no cover + + @overload + 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) - elif isinstance(other, datetime): + elif isinstance(other, dt_datetime): return self._datetime - other elif isinstance(other, Arrow): @@ -1451,53 +1573,53 @@ def __sub__(self, other): return NotImplemented - def __rsub__(self, other): + def __rsub__(self, other: Any) -> timedelta: - if isinstance(other, datetime): + if isinstance(other, dt_datetime): return other - self._datetime return NotImplemented # comparisons - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, datetime)): + if not isinstance(other, (Arrow, dt_datetime)): return False return self._datetime == self._get_datetime(other) - def __ne__(self, other): + def __ne__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, datetime)): + if not isinstance(other, (Arrow, dt_datetime)): return True return not self.__eq__(other) - def __gt__(self, other): + def __gt__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, datetime)): + if not isinstance(other, (Arrow, dt_datetime)): return NotImplemented return self._datetime > self._get_datetime(other) - def __ge__(self, other): + def __ge__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, datetime)): + if not isinstance(other, (Arrow, dt_datetime)): return NotImplemented return self._datetime >= self._get_datetime(other) - def __lt__(self, other): + def __lt__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, datetime)): + if not isinstance(other, (Arrow, dt_datetime)): return NotImplemented return self._datetime < self._get_datetime(other) - def __le__(self, other): + def __le__(self, other: Any) -> bool: - if not isinstance(other, (Arrow, datetime)): + if not isinstance(other, (Arrow, dt_datetime)): return NotImplemented return self._datetime <= self._get_datetime(other) @@ -1505,7 +1627,7 @@ def __le__(self, other): # internal methods @staticmethod - def _get_tzinfo(tz_expr): + def _get_tzinfo(tz_expr: Optional[TZ_EXPR]) -> dt_tzinfo: if tz_expr is None: return dateutil_tz.tzutc() @@ -1518,11 +1640,13 @@ def _get_tzinfo(tz_expr): raise ValueError(f"{tz_expr!r} not recognized as a timezone.") @classmethod - def _get_datetime(cls, expr): + def _get_datetime( + cls, expr: Union["Arrow", dt_datetime, int, float, str] + ) -> dt_datetime: """Get datetime object for a specified expression.""" if isinstance(expr, Arrow): return expr.datetime - elif isinstance(expr, datetime): + elif isinstance(expr, dt_datetime): return expr elif util.is_timestamp(expr): timestamp = float(expr) @@ -1531,7 +1655,7 @@ def _get_datetime(cls, expr): raise ValueError(f"{expr!r} not recognized as a datetime or timestamp.") @classmethod - def _get_frames(cls, name): + def _get_frames(cls, name: _T_FRAMES) -> Tuple[str, str, int]: if name in cls._ATTRS: return name, f"{name}s", 1 @@ -1560,7 +1684,7 @@ def _get_frames(cls, name): ) @classmethod - def _get_iteration_params(cls, end, limit): + def _get_iteration_params(cls, end: Any, limit: Optional[int]) -> Tuple[Any, int]: if end is None: @@ -1575,9 +1699,9 @@ def _get_iteration_params(cls, end, limit): return end, limit @staticmethod - def _is_last_day_of_month(date): + def _is_last_day_of_month(date: "Arrow") -> bool: return date.day == calendar.monthrange(date.year, date.month)[1] -Arrow.min = Arrow.fromdatetime(datetime.min) -Arrow.max = Arrow.fromdatetime(datetime.max) +Arrow.min = Arrow.fromdatetime(dt_datetime.min) +Arrow.max = Arrow.fromdatetime(dt_datetime.max) diff --git a/arrow/constants.py b/arrow/constants.py index afef30f6b..1bf36d6c3 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -1,6 +1,11 @@ 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 + # datetime.max.timestamp() errors on Windows, so we must hardcode # the highest possible datetime value that can output a timestamp. # tl;dr platform-independent max timestamps are hard to form @@ -8,20 +13,21 @@ try: # Get max timestamp. Works on POSIX-based systems like Linux and macOS, # but will trigger an OverflowError, ValueError, or OSError on Windows - MAX_TIMESTAMP = datetime.max.timestamp() + _MAX_TIMESTAMP = datetime.max.timestamp() except (OverflowError, ValueError, OSError): # pragma: no cover # Fallback for Windows if initial max timestamp call fails # 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 is_64bits = sys.maxsize > 2 ** 32 - MAX_TIMESTAMP = ( + _MAX_TIMESTAMP = ( datetime(3000, 12, 31, 23, 59, 59, 999999).timestamp() if is_64bits else datetime(2038, 1, 18, 23, 59, 59, 999999).timestamp() ) -MAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1000 -MAX_TIMESTAMP_US = MAX_TIMESTAMP * 1_000_000 +MAX_TIMESTAMP: Final[float] = _MAX_TIMESTAMP +MAX_TIMESTAMP_MS: Final[float] = MAX_TIMESTAMP * 1000 +MAX_TIMESTAMP_US: Final[float] = MAX_TIMESTAMP * 1_000_000 -MAX_ORDINAL = datetime.max.toordinal() -MIN_ORDINAL = 1 +MAX_ORDINAL: Final[int] = datetime.max.toordinal() +MIN_ORDINAL: Final[int] = 1 diff --git a/arrow/factory.py b/arrow/factory.py index 9aa44b8ee..231510f58 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -10,11 +10,12 @@ from datetime import date, datetime from datetime import tzinfo as dt_tzinfo 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 Arrow +from arrow.arrow import TZ_EXPR, Arrow from arrow.util import is_timestamp, iso_to_gregorian @@ -26,10 +27,67 @@ class ArrowFactory: """ - def __init__(self, type=Arrow): + type: Type[Arrow] + + def __init__(self, type: Type[Arrow] = Arrow) -> None: self.type = type - def get(self, *args, **kwargs): + @overload + def get( + self, + *, + locale: str = "en_us", + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, + ) -> Arrow: + ... # pragma: no cover + + @overload + def get( + self, + __obj: Union[ + Arrow, + datetime, + date, + struct_time, + dt_tzinfo, + int, + float, + str, + Tuple[int, int, int], + ], + *, + locale: str = "en_us", + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, + ) -> Arrow: + ... # pragma: no cover + + @overload + def get( + self, + __arg1: Union[datetime, date], + __arg2: TZ_EXPR, + *, + locale: str = "en_us", + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, + ) -> Arrow: + ... # pragma: no cover + + @overload + def get( + self, + __arg1: str, + __arg2: Union[str, List[str]], + *, + locale: str = "en_us", + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, + ) -> Arrow: + ... # pragma: no cover + + def get(self, *args: Any, **kwargs: Any) -> Arrow: """Returns an :class:`Arrow ` object based on flexible inputs. :param locale: (optional) a ``str`` specifying a locale for the parser. Defaults to 'en_us'. @@ -203,8 +261,8 @@ def get(self, *args, **kwargs): # (iso calendar) -> convert then from date elif isinstance(arg, tuple) and len(arg) == 3: - dt = iso_to_gregorian(*arg) - return self.type.fromdate(dt) + d = iso_to_gregorian(*arg) + return self.type.fromdate(d) else: raise TypeError(f"Cannot parse single argument of type {type(arg)!r}.") @@ -249,7 +307,7 @@ def get(self, *args, **kwargs): else: return self.type(*args, **kwargs) - def utcnow(self): + def utcnow(self) -> Arrow: """Returns an :class:`Arrow ` object, representing "now" in UTC time. Usage:: @@ -261,7 +319,7 @@ def utcnow(self): return self.type.utcnow() - def now(self, tz=None): + def now(self, tz: Optional[TZ_EXPR] = None) -> Arrow: """Returns an :class:`Arrow ` object, representing "now" in the given timezone. diff --git a/arrow/formatter.py b/arrow/formatter.py index 7fe5abbb3..14eb44a20 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -1,19 +1,28 @@ import re +import sys +from datetime import datetime, timedelta +from typing import Optional, Pattern, cast from dateutil import tz as dateutil_tz from arrow import locales -FORMAT_ATOM = "YYYY-MM-DD HH:mm:ssZZ" -FORMAT_COOKIE = "dddd, DD-MMM-YYYY HH:mm:ss ZZZ" -FORMAT_RFC822 = "ddd, DD MMM YY HH:mm:ss Z" -FORMAT_RFC850 = "dddd, DD-MMM-YY HH:mm:ss ZZZ" -FORMAT_RFC1036 = "ddd, DD MMM YY HH:mm:ss Z" -FORMAT_RFC1123 = "ddd, DD MMM YYYY HH:mm:ss Z" -FORMAT_RFC2822 = "ddd, DD MMM YYYY HH:mm:ss Z" -FORMAT_RFC3339 = "YYYY-MM-DD HH:mm:ssZZ" -FORMAT_RSS = "ddd, DD MMM YYYY HH:mm:ss Z" -FORMAT_W3C = "YYYY-MM-DD HH:mm:ssZZ" +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" +FORMAT_RFC850: Final[str] = "dddd, DD-MMM-YY HH:mm:ss ZZZ" +FORMAT_RFC1036: Final[str] = "ddd, DD MMM YY HH:mm:ss Z" +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_RSS: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z" +FORMAT_W3C: Final[str] = "YYYY-MM-DD HH:mm:ssZZ" class DateTimeFormatter: @@ -22,19 +31,24 @@ class DateTimeFormatter: # 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 - _FORMAT_RE = re.compile( + _FORMAT_RE: Final[Pattern[str]] = re.compile( r"(\[(?:(?=(?P[^]]))(?P=literal))*\]|YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X|x|W)" ) - def __init__(self, locale="en_us"): + locale: locales.Locale + + def __init__(self, locale: str = "en_us") -> None: self.locale = locales.get_locale(locale) - def format(cls, dt, fmt): + def format(cls, dt: datetime, fmt: str) -> str: - return cls._FORMAT_RE.sub(lambda m: cls._format_token(dt, m.group(0)), fmt) + # 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, token): + def _format_token(self, dt: datetime, token: Optional[str]) -> Optional[str]: if token and token.startswith("[") and token.endswith("]"): return token[1:-1] @@ -116,7 +130,10 @@ def _format_token(self, dt, token): if token in ["ZZ", "Z"]: separator = ":" if token == "ZZ" else "" tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo - total_minutes = int(tz.utcoffset(dt).total_seconds() / 60) + # `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 + total_minutes = int(cast(timedelta, tz.utcoffset(dt)).total_seconds() / 60) sign = "+" if total_minutes >= 0 else "-" total_minutes = abs(total_minutes) diff --git a/arrow/locales.py b/arrow/locales.py index ca3666a3e..f05227224 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1,9 +1,54 @@ import inspect import sys from math import trunc - - -def get_locale(name): +from typing import ( + Any, + ClassVar, + Dict, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + Union, + 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", + "seconds", + "minute", + "minutes", + "hour", + "hours", + "day", + "days", + "week", + "weeks", + "month", + "months", + "year", + "years", + "2-hours", + "2-days", + "2-weeks", + "2-months", + "2-years", +] + +_TimeFrameElements = Union[ + str, Sequence[str], Mapping[str, str], Mapping[str, Sequence[str]] +] + + +def get_locale(name: str) -> "Locale": """Returns an appropriate :class:`Locale ` corresponding to an input locale name. @@ -19,14 +64,14 @@ def get_locale(name): return locale_cls() -def get_locale_by_class_name(name): +def get_locale_by_class_name(name: str) -> "Locale": """Returns an appropriate :class:`Locale ` corresponding to an locale class name. :param name: the name of the locale class. """ - locale_cls = globals().get(name) + locale_cls: Optional[Type[Locale]] = globals().get(name) if locale_cls is None: raise ValueError(f"Unsupported locale {name!r}.") @@ -40,9 +85,9 @@ def get_locale_by_class_name(name): class Locale: """ Represents locale-specific data and functionality. """ - names = [] + names: ClassVar[List[str]] = [] - timeframes = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, _TimeFrameElements]] = { "now": "", "second": "", "seconds": "", @@ -60,25 +105,32 @@ class Locale: "years": "", } - meridians = {"am": "", "pm": "", "AM": "", "PM": ""} + meridians: ClassVar[Dict[str, str]] = {"am": "", "pm": "", "AM": "", "PM": ""} + + past: ClassVar[str] + future: ClassVar[str] + and_word: ClassVar[Optional[str]] = None - past = None - future = None - and_word = None + month_names: ClassVar[List[str]] = [] + month_abbreviations: ClassVar[List[str]] = [] - month_names = [] - month_abbreviations = [] + day_names: ClassVar[List[str]] = [] + day_abbreviations: ClassVar[List[str]] = [] - day_names = [] - day_abbreviations = [] + ordinal_day_re: ClassVar[str] = r"(\d+)" - ordinal_day_re = r"(\d+)" + _month_name_to_ordinal: Optional[Dict[str, int]] - def __init__(self): + def __init__(self) -> None: self._month_name_to_ordinal = None - def describe(self, timeframe, delta=0, only_distance=False): + def describe( + self, + timeframe: TimeFrameLiteral, + delta: Union[float, int] = 0, + only_distance: bool = False, + ) -> str: """Describes a delta within a timeframe in plain language. :param timeframe: a string representing a timeframe. @@ -92,7 +144,11 @@ def describe(self, timeframe, delta=0, only_distance=False): return humanized - def describe_multi(self, timeframes, only_distance=False): + def describe_multi( + self, + timeframes: Sequence[Tuple[TimeFrameLiteral, Union[int, float]]], + only_distance: bool = False, + ) -> str: """Describes a delta within multiple timeframes in plain language. :param timeframes: a list of string, quantity pairs each representing a timeframe and delta. @@ -111,7 +167,7 @@ def describe_multi(self, timeframes, only_distance=False): return humanized - def day_name(self, day): + def day_name(self, day: int) -> str: """Returns the day name for a specified day of the week. :param day: the ``int`` day of the week (1-7). @@ -120,7 +176,7 @@ def day_name(self, day): return self.day_names[day] - def day_abbreviation(self, day): + def day_abbreviation(self, day: int) -> str: """Returns the day abbreviation for a specified day of the week. :param day: the ``int`` day of the week (1-7). @@ -129,7 +185,7 @@ def day_abbreviation(self, day): return self.day_abbreviations[day] - def month_name(self, month): + def month_name(self, month: int) -> str: """Returns the month name for a specified month of the year. :param month: the ``int`` month of the year (1-12). @@ -138,7 +194,7 @@ def month_name(self, month): return self.month_names[month] - def month_abbreviation(self, month): + def month_abbreviation(self, month: int) -> str: """Returns the month abbreviation for a specified month of the year. :param month: the ``int`` month of the year (1-12). @@ -147,7 +203,7 @@ def month_abbreviation(self, month): return self.month_abbreviations[month] - def month_number(self, name): + def month_number(self, name: str) -> Optional[int]: """Returns the month number for a month specified by name or abbreviation. :param name: the month name or abbreviation. @@ -162,21 +218,21 @@ def month_number(self, name): return self._month_name_to_ordinal.get(name) - def year_full(self, year): + def year_full(self, year: int) -> str: """Returns the year for specific locale if available :param year: the ``int`` year (4-digit) """ return f"{year:04d}" - def year_abbreviation(self, year): + def year_abbreviation(self, year: int) -> str: """Returns the year for specific locale if available :param year: the ``int`` year (4-digit) """ return f"{year:04d}"[2:] - def meridian(self, hour, token): + def meridian(self, hour: int, token: Any) -> Optional[str]: """Returns the meridian indicator for a specified hour and format token. :param hour: the ``int`` hour of the day. @@ -187,24 +243,33 @@ def meridian(self, hour, token): return self.meridians["am"] if hour < 12 else self.meridians["pm"] if token == "A": return self.meridians["AM"] if hour < 12 else self.meridians["PM"] + return None - def ordinal_number(self, n): + def ordinal_number(self, n: int) -> str: """Returns the ordinal format of a given integer :param n: an integer """ return self._ordinal_number(n) - def _ordinal_number(self, n): + def _ordinal_number(self, n: int) -> str: return f"{n}" - def _name_to_ordinal(self, lst): + def _name_to_ordinal(self, lst: Sequence[str]) -> Dict[str, int]: return {elem.lower(): i for i, elem in enumerate(lst[1:], 1)} - def _format_timeframe(self, timeframe, delta): - return self.timeframes[timeframe].format(trunc(abs(delta))) + def _format_timeframe( + self, timeframe: TimeFrameLiteral, delta: Union[float, int] + ) -> str: + # TODO: remove cast + return cast(str, self.timeframes[timeframe]).format(trunc(abs(delta))) - def _format_relative(self, humanized, timeframe, delta): + def _format_relative( + self, + humanized: str, + timeframe: TimeFrameLiteral, + delta: Union[float, int], + ) -> str: if timeframe == "now": return humanized @@ -300,7 +365,7 @@ class EnglishLocale(Locale): ordinal_day_re = r"((?P[2-3]?1(?=st)|[2-3]?2(?=nd)|[2-3]?3(?=rd)|[1-3]?[04-9](?=th)|1[1-3](?=th))(st|nd|rd|th))" - def _ordinal_number(self, n): + def _ordinal_number(self, n: int) -> str: if n % 100 not in (11, 12, 13): remainder = abs(n) % 10 if remainder == 1: @@ -311,7 +376,12 @@ def _ordinal_number(self, n): return f"{n}rd" return f"{n}th" - def describe(self, timeframe, delta=0, only_distance=False): + def describe( + self, + timeframe: TimeFrameLiteral, + delta: Union[int, float] = 0, + only_distance: bool = False, + ) -> str: """Describes a delta within a timeframe in plain language. :param timeframe: a string representing a timeframe. @@ -395,7 +465,7 @@ class ItalianLocale(Locale): ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])" - def _ordinal_number(self, n): + def _ordinal_number(self, n: int) -> str: return f"{n}º" @@ -470,7 +540,7 @@ class SpanishLocale(Locale): ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])" - def _ordinal_number(self, n): + def _ordinal_number(self, n: int) -> str: return f"{n}º" @@ -530,7 +600,7 @@ class FrenchBaseLocale(Locale): r"((?P\b1(?=er\b)|[1-3]?[02-9](?=e\b)|[1-3]1(?=e\b))(er|e)\b)" ) - def _ordinal_number(self, n): + def _ordinal_number(self, n: int) -> str: if abs(n) == 1: return f"{n}er" return f"{n}e" @@ -787,7 +857,7 @@ class FinnishLocale(Locale): past = "{0} sitten" future = "{0} kuluttua" - timeframes = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, List[str]]] = { "now": ["juuri nyt", "juuri nyt"], "second": ["sekunti", "sekunti"], "seconds": ["{0} muutama sekunti", "{0} muutaman sekunnin"], @@ -849,13 +919,19 @@ class FinnishLocale(Locale): day_abbreviations = ["", "ma", "ti", "ke", "to", "pe", "la", "su"] - def _format_timeframe(self, timeframe, delta): + # TODO: Fix return type + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: Union[float, int]) -> Tuple[str, str]: # type: ignore return ( self.timeframes[timeframe][0].format(abs(delta)), self.timeframes[timeframe][1].format(abs(delta)), ) - def _format_relative(self, humanized, timeframe, delta): + def _format_relative( + self, + humanized: str, + timeframe: TimeFrameLiteral, + delta: Union[float, int], + ) -> str: if timeframe == "now": return humanized[0] @@ -864,7 +940,7 @@ def _format_relative(self, humanized, timeframe, delta): return direction.format(humanized[which]) - def _ordinal_number(self, n): + def _ordinal_number(self, n: int) -> str: return f"{n}." @@ -1120,19 +1196,24 @@ class KoreanLocale(Locale): day_names = ["", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"] day_abbreviations = ["", "월", "화", "수", "목", "금", "토", "일"] - def _ordinal_number(self, n): + def _ordinal_number(self, n: int) -> str: ordinals = ["0", "첫", "두", "세", "네", "다섯", "여섯", "일곱", "여덟", "아홉", "열"] if n < len(ordinals): - return "{}번째".format(ordinals[n]) + return f"{ordinals[n]}번째" return f"{n}번째" - def _format_relative(self, humanized, timeframe, delta): + def _format_relative( + self, + humanized: str, + timeframe: TimeFrameLiteral, + delta: Union[float, int], + ) -> str: if timeframe in ("day", "days"): - special = self.special_dayframes.get(delta) + special = self.special_dayframes.get(int(delta)) if special: return special elif timeframe in ("year", "years"): - special = self.special_yearframes.get(delta) + special = self.special_yearframes.get(int(delta)) if special: return special @@ -1212,13 +1293,15 @@ class DutchLocale(Locale): class SlavicBaseLocale(Locale): - def _format_timeframe(self, timeframe, delta): + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, List[str]]]] + def _format_timeframe( + self, timeframe: TimeFrameLiteral, delta: Union[float, int] + ) -> str: form = self.timeframes[timeframe] delta = abs(delta) if isinstance(form, list): - if delta % 10 == 1 and delta % 100 != 11: form = form[0] elif 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): @@ -1236,7 +1319,7 @@ class BelarusianLocale(SlavicBaseLocale): past = "{0} таму" future = "праз {0}" - timeframes = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, List[str]]]] = { "now": "зараз", "second": "секунду", "seconds": "{0} некалькі секунд", @@ -1305,7 +1388,7 @@ class PolishLocale(SlavicBaseLocale): # The nouns should be in genitive case (Polish: "dopełniacz") # in order to correctly form `past` & `future` expressions. - timeframes = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, List[str]]]] = { "now": "teraz", "second": "sekundę", "seconds": ["{0} sekund", "{0} sekundy", "{0} sekund"], @@ -1374,7 +1457,7 @@ class RussianLocale(SlavicBaseLocale): past = "{0} назад" future = "через {0}" - timeframes = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, List[str]]]] = { "now": "сейчас", "second": "Второй", "seconds": "{0} несколько секунд", @@ -1510,7 +1593,7 @@ class BulgarianLocale(SlavicBaseLocale): past = "{0} назад" future = "напред {0}" - timeframes = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, List[str]]]] = { "now": "сега", "second": "секунда", "seconds": "{0} няколко секунди", @@ -1577,7 +1660,7 @@ class UkrainianLocale(SlavicBaseLocale): past = "{0} тому" future = "за {0}" - timeframes = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, List[str]]]] = { "now": "зараз", "second": "секунда", "seconds": "{0} кілька секунд", @@ -1643,7 +1726,7 @@ class MacedonianLocale(SlavicBaseLocale): past = "пред {0}" future = "за {0}" - timeframes = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, List[str]]]] = { "now": "сега", "second": "една секунда", "seconds": ["{0} секунда", "{0} секунди", "{0} секунди"], @@ -1793,10 +1876,15 @@ class GermanBaseLocale(Locale): day_abbreviations = ["", "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] - def _ordinal_number(self, n): + def _ordinal_number(self, n: int) -> str: return f"{n}." - def describe(self, timeframe, delta=0, only_distance=False): + def describe( + self, + timeframe: TimeFrameLiteral, + delta: Union[int, float] = 0, + only_distance: bool = False, + ) -> str: """Describes a delta within a timeframe in plain language. :param timeframe: a string representing a timeframe. @@ -2123,7 +2211,7 @@ class TagalogLocale(Locale): meridians = {"am": "nu", "pm": "nh", "AM": "ng umaga", "PM": "ng hapon"} - def _ordinal_number(self, n): + def _ordinal_number(self, n: int) -> str: return f"ika-{n}" @@ -2355,7 +2443,7 @@ class ArabicLocale(Locale): past = "منذ {0}" future = "خلال {0}" - timeframes = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "الآن", "second": "ثانية", "seconds": {"double": "ثانيتين", "ten": "{0} ثوان", "higher": "{0} ثانية"}, @@ -2414,10 +2502,12 @@ class ArabicLocale(Locale): ] day_abbreviations = ["", "إثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت", "أحد"] - def _format_timeframe(self, timeframe, delta): + def _format_timeframe( + self, timeframe: TimeFrameLiteral, delta: Union[float, int] + ) -> str: form = self.timeframes[timeframe] delta = abs(delta) - if isinstance(form, dict): + if isinstance(form, Mapping): if delta == 2: form = form["double"] elif 2 < delta <= 10: @@ -2565,22 +2655,24 @@ class MoroccoArabicLocale(ArabicLocale): class IcelandicLocale(Locale): - def _format_timeframe(self, timeframe, delta): - - timeframe = self.timeframes[timeframe] + def _format_timeframe( + self, timeframe: TimeFrameLiteral, delta: Union[float, int] + ) -> str: + form = self.timeframes[timeframe] if delta < 0: - timeframe = timeframe[0] + form = form[0] elif delta > 0: - timeframe = timeframe[1] + form = form[1] + # FIXME: handle when delta is 0 - return timeframe.format(abs(delta)) + return form.format(abs(delta)) # type: ignore names = ["is", "is_is"] past = "fyrir {0} síðan" future = "eftir {0}" - timeframes = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[Tuple[str, str], str]]] = { "now": "rétt í þessu", "second": ("sekúndu", "sekúndu"), "seconds": ("{0} nokkrum sekúndum", "nokkrar sekúndur"), @@ -2856,7 +2948,9 @@ class HindiLocale(Locale): class CzechLocale(Locale): names = ["cs", "cs_cz"] - timeframes = { + timeframes: ClassVar[ + Mapping[TimeFrameLiteral, Union[Mapping[str, Union[List[str], str]], str]] + ] = { "now": "Teď", "second": {"past": "vteřina", "future": "vteřina", "zero": "vteřina"}, "seconds": {"past": "{0} sekundami", "future": ["{0} sekundy", "{0} sekund"]}, @@ -2920,32 +3014,42 @@ class CzechLocale(Locale): ] day_abbreviations = ["", "po", "út", "st", "čt", "pá", "so", "ne"] - def _format_timeframe(self, timeframe, delta): + def _format_timeframe( + self, timeframe: TimeFrameLiteral, delta: Union[float, int] + ) -> str: """Czech aware time frame format function, takes into account the differences between past and future forms.""" + abs_delta = abs(delta) form = self.timeframes[timeframe] - if isinstance(form, dict): - if delta == 0: - form = form["zero"] # And *never* use 0 in the singular! - elif delta > 0: - form = form["future"] - else: - form = form["past"] - delta = abs(delta) + + if isinstance(form, str): + return form.format(abs_delta) + + if delta == 0: + key = "zero" # And *never* use 0 in the singular! + elif delta > 0: + key = "future" + else: + key = "past" + form: Union[List[str], str] = form[key] if isinstance(form, list): - if 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): + if 2 <= abs_delta % 10 <= 4 and ( + abs_delta % 100 < 10 or abs_delta % 100 >= 20 + ): form = form[0] else: form = form[1] - return form.format(delta) + return form.format(abs_delta) class SlovakLocale(Locale): names = ["sk", "sk_sk"] - timeframes = { + timeframes: ClassVar[ + Mapping[TimeFrameLiteral, Union[Mapping[str, Union[List[str], str]], str]] + ] = { "now": "Teraz", "second": {"past": "sekundou", "future": "sekundu", "zero": "{0} sekúnd"}, "seconds": {"past": "{0} sekundami", "future": ["{0} sekundy", "{0} sekúnd"]}, @@ -3010,26 +3114,34 @@ class SlovakLocale(Locale): ] day_abbreviations = ["", "po", "ut", "st", "št", "pi", "so", "ne"] - def _format_timeframe(self, timeframe, delta): + def _format_timeframe( + self, timeframe: TimeFrameLiteral, delta: Union[float, int] + ) -> str: """Slovak aware time frame format function, takes into account the differences between past and future forms.""" + abs_delta = abs(delta) form = self.timeframes[timeframe] - if isinstance(form, dict): - if delta == 0: - form = form["zero"] # And *never* use 0 in the singular! - elif delta > 0: - form = form["future"] - else: - form = form["past"] - delta = abs(delta) + + if isinstance(form, str): + return form.format(abs_delta) + + if delta == 0: + key = "zero" # And *never* use 0 in the singular! + elif delta > 0: + key = "future" + else: + key = "past" + form: Union[List[str], str] = form[key] if isinstance(form, list): - if 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): + if 2 <= abs_delta % 10 <= 4 and ( + abs_delta % 100 < 10 or abs_delta % 100 >= 20 + ): form = form[0] else: form = form[1] - return form.format(delta) + return form.format(abs_delta) class FarsiLocale(Locale): @@ -3178,7 +3290,9 @@ class HebrewLocale(Locale): day_names = ["", "שני", "שלישי", "רביעי", "חמישי", "שישי", "שבת", "ראשון"] day_abbreviations = ["", "ב׳", "ג׳", "ד׳", "ה׳", "ו׳", "ש׳", "א׳"] - def _format_timeframe(self, timeframe, delta): + def _format_timeframe( + self, timeframe: TimeFrameLiteral, delta: Union[float, int] + ) -> str: """Hebrew couple of aware""" couple = f"2-{timeframe}" single = timeframe.rstrip("s") @@ -3191,7 +3305,11 @@ def _format_timeframe(self, timeframe, delta): return self.timeframes[key].format(trunc(abs(delta))) - def describe_multi(self, timeframes, only_distance=False): + def describe_multi( + self, + timeframes: Sequence[Tuple[TimeFrameLiteral, Union[int, float]]], + only_distance: bool = False, + ) -> str: """Describes a delta within multiple timeframes in plain language. In Hebrew, the and word behaves a bit differently. @@ -3287,9 +3405,9 @@ class MarathiLocale(Locale): day_abbreviations = ["", "सोम", "मंगळ", "बुध", "गुरु", "शुक्र", "शनि", "रवि"] -def _map_locales(): +def _map_locales() -> Dict[str, Type[Locale]]: - locales = {} + locales: Dict[str, Type[Locale]] = {} for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): if issubclass(cls, Locale): # pragma: no branch @@ -3444,7 +3562,7 @@ class HungarianLocale(Locale): past = "{0} ezelőtt" future = "{0} múlva" - timeframes = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "éppen most", "second": {"past": "egy második", "future": "egy második"}, "seconds": {"past": "{0} másodpercekkel", "future": "{0} pár másodperc"}, @@ -3505,10 +3623,12 @@ class HungarianLocale(Locale): meridians = {"am": "de", "pm": "du", "AM": "DE", "PM": "DU"} - def _format_timeframe(self, timeframe, delta): + def _format_timeframe( + self, timeframe: TimeFrameLiteral, delta: Union[float, int] + ) -> str: form = self.timeframes[timeframe] - if isinstance(form, dict): + if isinstance(form, Mapping): if delta > 0: form = form["future"] else: @@ -3585,7 +3705,7 @@ class EsperantoLocale(Locale): ordinal_day_re = r"((?P[1-3]?[0-9](?=a))a)" - def _ordinal_number(self, n): + def _ordinal_number(self, n: int) -> str: return f"{n}a" @@ -3650,17 +3770,22 @@ class ThaiLocale(Locale): BE_OFFSET = 543 - def year_full(self, year): + def year_full(self, year: int) -> str: """Thai always use Buddhist Era (BE) which is CE + 543""" year += self.BE_OFFSET return f"{year:04d}" - def year_abbreviation(self, year): + def year_abbreviation(self, year: int) -> str: """Thai always use Buddhist Era (BE) which is CE + 543""" year += self.BE_OFFSET return f"{year:04d}"[2:] - def _format_relative(self, humanized, timeframe, delta): + def _format_relative( + self, + humanized: str, + timeframe: TimeFrameLiteral, + delta: Union[float, int], + ) -> str: """Thai normally doesn't have any space between words""" if timeframe == "now": return humanized @@ -3738,7 +3863,7 @@ class BengaliLocale(Locale): ] day_abbreviations = ["", "সোম", "মঙ্গল", "বুধ", "বৃহঃ", "শুক্র", "শনি", "রবি"] - def _ordinal_number(self, n): + def _ordinal_number(self, n: int) -> str: if n > 10 or n == 0: return f"{n}তম" if n in [1, 5, 7, 8, 9, 10]: @@ -4106,7 +4231,7 @@ class EstonianLocale(Locale): future = "{0} pärast" and_word = "ja" - timeframes = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Mapping[str, str]]] = { "now": {"past": "just nüüd", "future": "just nüüd"}, "second": {"past": "üks sekund", "future": "ühe sekundi"}, "seconds": {"past": "{0} sekundit", "future": "{0} sekundi"}, @@ -4165,13 +4290,15 @@ class EstonianLocale(Locale): ] day_abbreviations = ["", "Esm", "Teis", "Kolm", "Nelj", "Re", "Lau", "Püh"] - def _format_timeframe(self, timeframe, delta): + def _format_timeframe( + self, timeframe: TimeFrameLiteral, delta: Union[float, int] + ) -> str: form = self.timeframes[timeframe] if delta > 0: - form = form["future"] + _form = form["future"] else: - form = form["past"] - return form.format(abs(delta)) + _form = form["past"] + return _form.format(abs(delta)) class SwahiliLocale(Locale): @@ -4259,4 +4386,4 @@ class SwahiliLocale(Locale): ] -_locales = _map_locales() +_locales: Dict[str, Type[Locale]] = _map_locales() diff --git a/arrow/parser.py b/arrow/parser.py index f39834e89..a6a617dbc 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -1,12 +1,35 @@ import re +import sys from datetime import datetime, timedelta +from datetime import tzinfo as dt_tzinfo from functools import lru_cache +from typing import ( + Any, + ClassVar, + Dict, + Iterable, + List, + Match, + Optional, + Pattern, + SupportsFloat, + SupportsInt, + Tuple, + Union, + cast, + overload, +) from dateutil import tz from arrow import locales 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 @@ -21,30 +44,87 @@ class ParserMatchError(ParserError): pass -class DateTimeParser: +_WEEKDATE_ELEMENT = Union[str, bytes, SupportsInt, bytearray] + +_FORMAT_TYPE = Literal[ + "YYYY", + "YY", + "MM", + "M", + "DDDD", + "DDD", + "DD", + "D", + "HH", + "H", + "hh", + "h", + "mm", + "m", + "ss", + "s", + "X", + "x", + "ZZZ", + "ZZ", + "Z", + "S", + "W", + "MMMM", + "MMM", + "Do", + "dddd", + "ddd", + "d", + "a", + "A", +] + + +class _Parts(TypedDict, total=False): + year: int + month: int + day_of_year: int + day: int + hour: int + minute: int + second: int + microsecond: int + timestamp: float + expanded_timestamp: int + tzinfo: dt_tzinfo + am_pm: Literal["am", "pm"] + day_of_week: int + weekdate: Tuple[_WEEKDATE_ELEMENT, _WEEKDATE_ELEMENT, Optional[_WEEKDATE_ELEMENT]] + - _FORMAT_RE = re.compile( +class DateTimeParser: + _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)" ) - _ESCAPE_RE = re.compile(r"\[[^\[\]]*\]") - - _ONE_OR_TWO_DIGIT_RE = re.compile(r"\d{1,2}") - _ONE_OR_TWO_OR_THREE_DIGIT_RE = re.compile(r"\d{1,3}") - _ONE_OR_MORE_DIGIT_RE = re.compile(r"\d+") - _TWO_DIGIT_RE = re.compile(r"\d{2}") - _THREE_DIGIT_RE = re.compile(r"\d{3}") - _FOUR_DIGIT_RE = re.compile(r"\d{4}") - _TZ_Z_RE = re.compile(r"([\+\-])(\d{2})(?:(\d{2}))?|Z") - _TZ_ZZ_RE = re.compile(r"([\+\-])(\d{2})(?:\:(\d{2}))?|Z") - _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") + _ESCAPE_RE: ClassVar[Pattern[str]] = re.compile(r"\[[^\[\]]*\]") + + _ONE_OR_TWO_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{1,2}") + _ONE_OR_TWO_OR_THREE_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{1,3}") + _ONE_OR_MORE_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d+") + _TWO_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{2}") + _THREE_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{3}") + _FOUR_DIGIT_RE: ClassVar[Pattern[str]] = re.compile(r"\d{4}") + _TZ_Z_RE: ClassVar[Pattern[str]] = re.compile(r"([\+\-])(\d{2})(?:(\d{2}))?|Z") + _TZ_ZZ_RE: ClassVar[Pattern[str]] = re.compile(r"([\+\-])(\d{2})(?:\:(\d{2}))?|Z") + _TZ_NAME_RE: ClassVar[Pattern[str]] = re.compile(r"\w[\w+\-/]+") # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will # break cases like "15 Jul 2000" and a format list (see issue #447) - _TIMESTAMP_RE = re.compile(r"^\-?\d+\.?\d+$") - _TIMESTAMP_EXPANDED_RE = re.compile(r"^\-?\d+$") - _TIME_RE = re.compile(r"^(\d{2})(?:\:?(\d{2}))?(?:\:?(\d{2}))?(?:([\.\,])(\d+))?$") - _WEEK_DATE_RE = re.compile(r"(?P\d{4})[\-]?W(?P\d{2})[\-]?(?P\d)?") + _TIMESTAMP_RE: ClassVar[Pattern[str]] = re.compile(r"^\-?\d+\.?\d+$") + _TIMESTAMP_EXPANDED_RE: ClassVar[Pattern[str]] = re.compile(r"^\-?\d+$") + _TIME_RE: ClassVar[Pattern[str]] = re.compile( + r"^(\d{2})(?:\:?(\d{2}))?(?:\:?(\d{2}))?(?:([\.\,])(\d+))?$" + ) + _WEEK_DATE_RE: ClassVar[Pattern[str]] = re.compile( + r"(?P\d{4})[\-]?W(?P\d{2})[\-]?(?P\d)?" + ) - _BASE_INPUT_RE_MAP = { + _BASE_INPUT_RE_MAP: ClassVar[Dict[_FORMAT_TYPE, Pattern[str]]] = { "YYYY": _FOUR_DIGIT_RE, "YY": _TWO_DIGIT_RE, "MM": _TWO_DIGIT_RE, @@ -70,9 +150,12 @@ class DateTimeParser: "W": _WEEK_DATE_RE, } - SEPARATORS = ["-", "/", "."] + SEPARATORS: ClassVar[List[str]] = ["-", "/", "."] + + locale: locales.Locale + _input_re_map: Dict[_FORMAT_TYPE, Pattern[str]] - def __init__(self, locale="en_us", cache_size=0): + def __init__(self, locale: str = "en_us", cache_size: int = 0) -> None: self.locale = locales.get_locale(locale) self._input_re_map = self._BASE_INPUT_RE_MAP.copy() @@ -101,13 +184,15 @@ def __init__(self, locale="en_us", cache_size=0): } ) if cache_size > 0: - self._generate_pattern_re = lru_cache(maxsize=cache_size)( + self._generate_pattern_re = lru_cache(maxsize=cache_size)( # type: ignore self._generate_pattern_re ) # TODO: since we support more than ISO 8601, we should rename this function # IDEA: break into multiple functions - def parse_iso(self, datetime_string, normalize_whitespace=False): + def parse_iso( + self, datetime_string: str, normalize_whitespace: bool = False + ) -> datetime: if normalize_whitespace: datetime_string = re.sub(r"\s+", " ", datetime_string.strip()) @@ -156,7 +241,7 @@ def parse_iso(self, datetime_string, normalize_whitespace=False): time_parts = re.split(r"[\+\-Z]", time_string, 1, re.IGNORECASE) - time_components = self._TIME_RE.match(time_parts[0]) + time_components: Optional[Match[str]] = self._TIME_RE.match(time_parts[0]) if time_components is None: raise ParserError( @@ -209,7 +294,12 @@ def parse_iso(self, datetime_string, normalize_whitespace=False): return self._parse_multiformat(datetime_string, formats) - def parse(self, datetime_string, fmt, normalize_whitespace=False): + def parse( + self, + datetime_string: str, + fmt: Union[List[str], str], + normalize_whitespace: bool = False, + ) -> datetime: if normalize_whitespace: datetime_string = re.sub(r"\s+", " ", datetime_string) @@ -218,6 +308,8 @@ def parse(self, datetime_string, fmt, normalize_whitespace=False): return self._parse_multiformat(datetime_string, fmt) try: + fmt_tokens: List[_FORMAT_TYPE] + fmt_pattern_re: Pattern[str] fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt) except re.error as e: raise ParserMatchError( @@ -231,8 +323,9 @@ def parse(self, datetime_string, fmt, normalize_whitespace=False): f"Failed to match {fmt!r} when parsing {datetime_string!r}." ) - parts = {} + parts: _Parts = {} for token in fmt_tokens: + value: Union[Tuple[str, str, str], str] if token == "Do": value = match.group("value") elif token == "W": @@ -245,17 +338,17 @@ def parse(self, datetime_string, fmt, normalize_whitespace=False): f"Unable to find a match group for the specified token {token!r}." ) - self._parse_token(token, value, parts) + self._parse_token(token, value, parts) # type: ignore return self._build_datetime(parts) - def _generate_pattern_re(self, fmt): + 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: # 'YYYY-MM-DD' -> '(?P\d{4})-(?P\d{2})-(?P
\d{2})' - tokens = [] + tokens: List[_FORMAT_TYPE] = [] offset = 0 # Escape all special RegEx chars @@ -273,7 +366,7 @@ def _generate_pattern_re(self, fmt): fmt_pattern = escaped_fmt for m in self._FORMAT_RE.finditer(escaped_fmt): - token = m.group(0) + token: _FORMAT_TYPE = cast(_FORMAT_TYPE, m.group(0)) try: input_re = self._input_re_map[token] except KeyError: @@ -313,12 +406,17 @@ def _generate_pattern_re(self, fmt): # see the documentation. starting_word_boundary = ( - r"(?\s])" # This is the list of punctuation that is ok before the pattern (i.e. "It can't not be these characters before the pattern") - r"(\b|^)" # The \b is to block cases like 1201912 but allow 201912 for pattern YYYYMM. The ^ was necessary to allow a negative number through i.e. before epoch numbers + r"(?\s])" # This is the list of punctuation that is ok before the + # pattern (i.e. "It can't not be these characters before the pattern") + r"(\b|^)" + # The \b is to block cases like 1201912 but allow 201912 for pattern YYYYMM. The ^ was necessary to allow a + # negative number through i.e. before epoch numbers ) ending_word_boundary = ( - r"(?=[\,\.\;\:\?\!\"\'\`\[\]\{\}\(\)\<\>]?" # Positive lookahead stating that these punctuation marks can appear after the pattern at most 1 time + r"(?=[\,\.\;\:\?\!\"\'\`\[\]\{\}\(\)\<\>]?" # Positive lookahead stating that these punctuation marks + # can appear after the pattern at most 1 time r"(?!\S))" # Don't allow any non-whitespace character after the punctuation ) bounded_fmt_pattern = r"{}{}{}".format( @@ -327,7 +425,76 @@ def _generate_pattern_re(self, fmt): return tokens, re.compile(bounded_fmt_pattern, flags=re.IGNORECASE) - def _parse_token(self, token, value, parts): + @overload + def _parse_token( + self, + token: Literal[ + "YYYY", + "YY", + "MM", + "M", + "DDDD", + "DDD", + "DD", + "D", + "Do", + "HH", + "hh", + "h", + "H", + "mm", + "m", + "ss", + "s", + "x", + ], + value: Union[str, bytes, SupportsInt, bytearray], + parts: _Parts, + ) -> None: + ... # pragma: no cover + + @overload + def _parse_token( + self, + token: Literal["X"], + value: Union[str, bytes, SupportsFloat, bytearray], + parts: _Parts, + ) -> None: + ... # pragma: no cover + + @overload + def _parse_token( + self, + token: Literal["MMMM", "MMM", "dddd", "ddd", "S"], + value: Union[str, bytes, bytearray], + parts: _Parts, + ) -> None: + ... # pragma: no cover + + @overload + def _parse_token( + self, + token: Literal["a", "A", "ZZZ", "ZZ", "Z"], + value: Union[str, bytes], + parts: _Parts, + ) -> None: + ... # pragma: no cover + + @overload + def _parse_token( + self, + token: Literal["W"], + value: Tuple[_WEEKDATE_ELEMENT, _WEEKDATE_ELEMENT, Optional[_WEEKDATE_ELEMENT]], + parts: _Parts, + ) -> None: + ... # pragma: no cover + + def _parse_token( + self, + token: Any, + value: Any, + parts: _Parts, + ) -> None: if token == "YYYY": parts["year"] = int(value) @@ -337,7 +504,8 @@ def _parse_token(self, token, value, parts): parts["year"] = 1900 + value if value > 68 else 2000 + value elif token in ["MMMM", "MMM"]: - parts["month"] = self.locale.month_number(value.lower()) + # FIXME: month_number() is nullable + parts["month"] = self.locale.month_number(value.lower()) # type: ignore elif token in ["MM", "M"]: parts["month"] = int(value) @@ -410,8 +578,7 @@ def _parse_token(self, token, value, parts): parts["weekdate"] = value @staticmethod - def _build_datetime(parts): - + def _build_datetime(parts: _Parts) -> datetime: weekdate = parts.get("weekdate") if weekdate is not None: @@ -419,12 +586,12 @@ def _build_datetime(parts): year, week = int(weekdate[0]), int(weekdate[1]) if weekdate[2] is not None: - day = int(weekdate[2]) + _day = int(weekdate[2]) else: # day not given, default to 1 - day = 1 + _day = 1 - date_string = f"{year}-{week}-{day}" + date_string = f"{year}-{week}-{_day}" # tokens for ISO 8601 weekdates dt = datetime.strptime(date_string, "%G-%V-%u") @@ -449,9 +616,9 @@ def _build_datetime(parts): day_of_year = parts.get("day_of_year") if day_of_year is not None: - year = parts.get("year") + _year = parts.get("year") month = parts.get("month") - if year is None: + if _year is None: raise ParserError( "Year component is required with the DDD and DDDD tokens." ) @@ -461,7 +628,7 @@ def _build_datetime(parts): "Month component is not allowed with the DDD and DDDD tokens." ) - date_string = f"{year}-{day_of_year}" + date_string = f"{_year}-{day_of_year}" try: dt = datetime.strptime(date_string, "%Y-%j") except ValueError: @@ -473,7 +640,7 @@ def _build_datetime(parts): parts["month"] = dt.month parts["day"] = dt.day - day_of_week = parts.get("day_of_week") + day_of_week: Optional[int] = parts.get("day_of_week") day = parts.get("day") # If day is passed, ignore day of week @@ -538,9 +705,9 @@ def _build_datetime(parts): + increment ) - def _parse_multiformat(self, string, formats): + def _parse_multiformat(self, string: str, formats: Iterable[str]) -> datetime: - _datetime = None + _datetime: Optional[datetime] = None for fmt in formats: try: @@ -559,17 +726,21 @@ def _parse_multiformat(self, string, formats): # generates a capture group of choices separated by an OR operator @staticmethod - def _generate_choice_re(choices, flags=0): + def _generate_choice_re( + choices: Iterable[str], flags: Union[int, re.RegexFlag] = 0 + ) -> Pattern[str]: return re.compile(r"({})".format("|".join(choices)), flags=flags) class TzinfoParser: - _TZINFO_RE = re.compile(r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$") + _TZINFO_RE: ClassVar[Pattern[str]] = re.compile( + r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$" + ) @classmethod - def parse(cls, tzinfo_string): + def parse(cls, tzinfo_string: str) -> dt_tzinfo: - tzinfo = None + tzinfo: Optional[dt_tzinfo] = None if tzinfo_string == "local": tzinfo = tz.tzlocal() @@ -582,10 +753,11 @@ def parse(cls, tzinfo_string): iso_match = cls._TZINFO_RE.match(tzinfo_string) if iso_match: + sign: Optional[str] + hours: str + minutes: Union[str, int, None] sign, hours, minutes = iso_match.groups() - if minutes is None: - minutes = 0 - seconds = int(hours) * 3600 + int(minutes) * 60 + seconds = int(hours) * 3600 + int(minutes or 0) * 60 if sign == "-": seconds *= -1 diff --git a/arrow/util.py b/arrow/util.py index 39381be5e..8679131ee 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -1,4 +1,5 @@ import datetime +from typing import Any, Optional, cast from dateutil.rrule import WEEKLY, rrule @@ -11,7 +12,9 @@ ) -def next_weekday(start_date, weekday): +def next_weekday( + start_date: Optional[datetime.date], weekday: int +) -> datetime.datetime: """Get next weekday from the specified start date. :param start_date: Datetime object representing the start date. @@ -34,10 +37,13 @@ def next_weekday(start_date, weekday): """ if weekday < 0 or weekday > 6: raise ValueError("Weekday must be between 0 (Monday) and 6 (Sunday).") - return rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0] + return cast( + datetime.datetime, + rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0], + ) -def is_timestamp(value): +def is_timestamp(value: Any) -> bool: """Check if value is a valid timestamp.""" if isinstance(value, bool): return False @@ -50,7 +56,7 @@ def is_timestamp(value): return False -def validate_ordinal(value): +def validate_ordinal(value: Any) -> None: """Raise the corresponding exception if value is an invalid Gregorian ordinal.""" if isinstance(value, bool) or not isinstance(value, int): raise TypeError(f"Ordinal must be an integer (got type {type(value)}).") @@ -58,7 +64,7 @@ def validate_ordinal(value): raise ValueError(f"Ordinal {value} is out of range.") -def normalize_timestamp(timestamp): +def normalize_timestamp(timestamp: float) -> float: """Normalize millisecond and microsecond timestamps into normal timestamps.""" if timestamp > MAX_TIMESTAMP: if timestamp < MAX_TIMESTAMP_MS: @@ -71,7 +77,7 @@ def normalize_timestamp(timestamp): # Credit to https://stackoverflow.com/a/1700069 -def iso_to_gregorian(iso_year, iso_week, iso_day): +def iso_to_gregorian(iso_year: int, iso_week: int, iso_day: int) -> datetime.date: """Converts an ISO week date tuple into a datetime object.""" if not 1 <= iso_week <= 53: @@ -89,7 +95,7 @@ def iso_to_gregorian(iso_year, iso_week, iso_day): return gregorian -def validate_bounds(bounds): +def validate_bounds(bounds: str) -> None: if bounds != "()" and bounds != "(]" and bounds != "[)" and bounds != "[]": raise ValueError( "Invalid bounds. Please select between '()', '(]', '[)', or '[]'." diff --git a/docs/conf.py b/docs/conf.py index ce16c080e..c12d44294 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,4 @@ +# mypy: ignore-errors # -- Path setup -------------------------------------------------------------- import os diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..bfb63588d --- /dev/null +++ b/setup.cfg @@ -0,0 +1,34 @@ +[mypy] +python_version = 3.6 + +allow_any_expr = True +allow_any_decorated = True +allow_any_explicit = True +disallow_any_generics = True +disallow_subclassing_any = True + +disallow_untyped_calls = True +disallow_untyped_defs = True +disallow_incomplete_defs = True +disallow_untyped_decorators = True + +no_implicit_optional = True + +warn_redundant_casts = True +warn_unused_ignores = True +no_warn_no_return = True +warn_return_any = True +warn_unreachable = True + +strict_equality = True +no_implicit_reexport = True + +## + +allow_redefinition = True + + +# Type annotation for test codes and migration files are not mandatory + +[mypy-*.tests.*,tests.*] +ignore_errors = True diff --git a/setup.py b/setup.py index 703f1c548..c25b782ca 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +# mypy: ignore-errors from setuptools import setup with open("README.rst", encoding="utf-8") as f: @@ -20,7 +21,10 @@ packages=["arrow"], zip_safe=False, python_requires=">=3.6", - install_requires=["python-dateutil>=2.7.0"], + install_requires=[ + "python-dateutil>=2.7.0", + "typing_extensions; python_version<'3.8'", + ], classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", From 37f4dbb5188f61cc8194992810f3b9b761b4e37d Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 18 Feb 2021 15:21:07 -0500 Subject: [PATCH 492/649] Update dependencies and add type annotations to sphinx docs (#919) --- .pre-commit-config.yaml | 11 +++++++---- LICENSE | 2 +- README.rst | 18 +++++++++--------- docs/conf.py | 4 ++-- requirements.txt | 9 +++++---- tox.ini | 1 + 6 files changed, 25 insertions(+), 20 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25a7fb0ca..e00406c8a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,20 +16,23 @@ repos: - id: check-merge-conflict - id: debug-statements - repo: https://github.com/timothycrosley/isort - rev: 5.6.4 + rev: 5.7.0 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.7.4 + rev: v2.10.0 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.7.0 + rev: v1.7.1 hooks: - id: python-no-eval - id: python-check-blanket-noqa + - id: python-use-type-annotations - id: rst-backticks + - id: rst-directive-colons + - id: rst-inline-touching-normal - repo: https://github.com/psf/black rev: 20.8b1 hooks: @@ -41,6 +44,6 @@ repos: - id: flake8 additional_dependencies: [flake8-bugbear] - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.790' + rev: 'v0.800' hooks: - id: mypy diff --git a/LICENSE b/LICENSE index 2bef500de..4f9eea5d1 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2019 Chris Smith + Copyright 2021 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/README.rst b/README.rst index 3571c71e4..bae1ba019 100644 --- a/README.rst +++ b/README.rst @@ -47,16 +47,16 @@ Features -------- - Fully-implemented, drop-in replacement for datetime -- Supports Python 3.6+ +- Support for Python 3.6+ - Timezone-aware and UTC by default -- Provides super-simple creation options for many common input scenarios -- :code:`shift` method with support for relative offsets, including weeks -- Formats and parses strings automatically -- Wide support for ISO 8601 +- Super-simple creation options for many common input scenarios +- ``shift`` method with support for relative offsets, including weeks +- Format and parse strings automatically +- Wide support for the `ISO 8601 `_ standard - Timezone conversion -- Timestamp available as a property +- Support for ``dateutil``, ``pytz``, and ``ZoneInfo`` tzinfo objects - Generates time spans, ranges, floors and ceilings for time frames ranging from microsecond to year -- Humanizes and supports a growing list of contributed locales +- Humanize dates and times with a growing list of contributed locales - Extensible for your own Arrow-derived types Quick Start @@ -122,10 +122,10 @@ Contributions are welcome for both code and localizations (adding and updating l #. Find an issue or feature to tackle on the `issue tracker `_. Issues marked with the `"good first issue" label `_ may be a great place to start! #. Fork `this repository `_ on GitHub and begin making changes in a branch. #. Add a few tests to ensure that the bug was fixed or the feature works as expected. -#. Run the entire test suite and linting checks by running one of the following commands: :code:`tox` (if you have `tox `_ installed) **OR** :code:`make build38 && make test && make lint` (if you do not have Python 3.8 installed, replace :code:`build38` with the latest Python version on your system). +#. Run the entire test suite and linting checks by running one of the following commands: ``tox && tox -e lint,docs`` (if you have `tox `_ installed) **OR** ``make build39 && make test && make lint`` (if you do not have Python 3.9 installed, replace ``build39`` with the latest Python version on your system). #. Submit a pull request and await feedback 😃. -If you have any questions along the way, feel free to ask them `here `_. +If you have any questions along the way, feel free to ask them `here `_. Support Arrow ------------- diff --git a/docs/conf.py b/docs/conf.py index c12d44294..907d78c0c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,14 +13,14 @@ # -- Project information ----------------------------------------------------- project = "Arrow 🏹" -copyright = "2020, Chris Smith" +copyright = "2021, Chris Smith" author = "Chris Smith" release = about["__version__"] # -- General configuration --------------------------------------------------- -extensions = ["sphinx.ext.autodoc"] +extensions = ["sphinx.ext.autodoc", "sphinx_autodoc_typehints"] templates_path = [] diff --git a/requirements.txt b/requirements.txt index ce27d0a12..002349a56 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ dateparser==1.0.* -pre-commit==2.9.* +pre-commit==2.10.* pytest==6.2.* -pytest-cov==2.10.* -pytest-mock==3.4.* +pytest-cov==2.11.* +pytest-mock==3.5.* python-dateutil==2.8.* pytz==2019.* simplejson==3.17.* -sphinx==3.4.* +sphinx==3.5.* +sphinx-autodoc-typehints==1.11.* diff --git a/tox.ini b/tox.ini index 492b09eb7..4faae0236 100644 --- a/tox.ini +++ b/tox.ini @@ -31,6 +31,7 @@ changedir = docs deps = doc8 sphinx + sphinx-autodoc-typehints python-dateutil allowlist_externals = make commands = From 87cd4a981f6b5f8f7e0f2039a79cc666199babac Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Sat, 20 Feb 2021 21:03:15 +0000 Subject: [PATCH 493/649] Remove usage of dateutil enfold() (#921) --- arrow/arrow.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index f2a2d27c2..dda7b4056 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -171,10 +171,8 @@ def __init__( fold = kwargs.get("fold", 0) - # use enfold here to cover direct arrow.Arrow init on 2.7/3.5 - self._datetime = dateutil_tz.enfold( # type: ignore - dt_datetime(year, month, day, hour, minute, second, microsecond, tzinfo), - fold=fold, + self._datetime = dt_datetime( + year, month, day, hour, minute, second, microsecond, tzinfo, fold=fold ) # factories: single object, both original and from datetime. @@ -960,9 +958,8 @@ def replace(self, **kwargs: Any) -> "Arrow": fold = kwargs.get("fold") - # TODO revisit this once we drop support for 2.7/3.5 if fold is not None: - current = dateutil_tz.enfold(current, fold=fold) # type: ignore + current = current.replace(fold=fold) return self.fromdatetime(current) From f15a7ae787b979e651d433073b7db8d672dbbcd4 Mon Sep 17 00:00:00 2001 From: Konstantin Weddige Date: Tue, 23 Feb 2021 19:52:36 +0100 Subject: [PATCH 494/649] Fix German time frames (#922) The rules for German timeframes for only_distance=True where incomplete. --- arrow/locales.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index f05227224..73b57533c 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1807,7 +1807,7 @@ class GermanBaseLocale(Locale): timeframes = { "now": "gerade eben", - "second": "eine Sekunde", + "second": "einer Sekunde", "seconds": "{0} Sekunden", "minute": "einer Minute", "minutes": "{0} Minuten", @@ -1824,12 +1824,16 @@ class GermanBaseLocale(Locale): } timeframes_only_distance = timeframes.copy() + timeframes_only_distance["second"] = "eine Sekunde" timeframes_only_distance["minute"] = "eine Minute" timeframes_only_distance["hour"] = "eine Stunde" timeframes_only_distance["day"] = "ein Tag" + timeframes_only_distance["days"] = "{0} Tage" timeframes_only_distance["week"] = "eine Woche" timeframes_only_distance["month"] = "ein Monat" + timeframes_only_distance["months"] = "{0} Monate" timeframes_only_distance["year"] = "ein Jahr" + timeframes_only_distance["years"] = "{0} Jahre" month_names = [ "", From 5248407fda7b2f35c5329f54b6eeb37570dd031c Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Thu, 25 Feb 2021 10:58:42 +0000 Subject: [PATCH 495/649] Bump version to 1.0.0 and update CHANGELOG (#924) * Start drafting changelog for 1.0.0 release * Increment version and continue drafting changelog * Changes based on comments --- CHANGELOG.rst | 29 +++++++++++++++++++++++++++++ arrow/_version.py | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0b55a4522..41d6ca56b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,35 @@ Changelog ========= +1.0.0 (2021-02-24) +------------------ + +After 8 years we're pleased to announce Arrow v1.0. Thanks to the entire Python community for helping make Arrow the amazing package it is today! + +- [CHANGE] Arrow has **dropped support** for Python 2.7 and 3.5. +- [CHANGE] There are multiple **breaking changes** with this release, please see the `migration guide `_ for a complete overview. +- [CHANGE] Arrow is now following `semantic versioning `_. +- [CHANGE] Made ``humanize`` granularity="auto" limits more accurate to reduce strange results. +- [NEW] Added support for Python 3.9. +- [NEW] Added a new keyword argument "exact" to ``span``, ``span_range`` and ``interval`` methods. This makes timespans begin at the start time given and not extend beyond the end time given, for example: + +..code-block:: python + >>> start = Arrow(2021, 2, 5, 12, 30) + >>> end = Arrow(2021, 2, 5, 17, 15) + >>> for r in arrow.Arrow.span_range('hour', start, end, exact=True): + ... print(r) + ... + (, ) + (, ) + (, ) + (, ) + (, ) + +- [NEW] Arrow now natively supports PEP 484-style type annotations. +- [FIX] Fixed handling of maximum permitted timestamp on Windows systems. +- [FIX] Corrections to French, German, Japanese and Norwegian locales. +- [INTERNAL] Raise more appropriate errors when string parsing fails to match. + 0.17.0 (2020-10-2) ------------------- diff --git a/arrow/_version.py b/arrow/_version.py index fd86b3ee9..5becc17c0 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "0.17.0" +__version__ = "1.0.0" From 7bc67846a34415845f5f09b688227965e32adb64 Mon Sep 17 00:00:00 2001 From: Aniruddha Maru Date: Fri, 26 Feb 2021 18:18:26 -0800 Subject: [PATCH 496/649] Add py.typed export per PEP 561 (#925) So that type testing tools like mypy can actually use type annotations --- arrow/py.typed | 0 setup.py | 1 + 2 files changed, 1 insertion(+) create mode 100644 arrow/py.typed diff --git a/arrow/py.typed b/arrow/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/setup.py b/setup.py index c25b782ca..14dff60db 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,7 @@ author_email="crsmithdev@gmail.com", license="Apache 2.0", packages=["arrow"], + package_data={"arrow": ["py.typed"]}, zip_safe=False, python_requires=">=3.6", install_requires=[ From 23800a8e4aabaf4d3e9037b5d37b330ee0df2041 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 27 Feb 2021 13:09:29 -0500 Subject: [PATCH 497/649] Bump version to 1.0.1 and update CHANGELOG (#927) * Bump version and updated CHANGELOG * Fix changelog RST * Add type hints to features. --- CHANGELOG.rst | 16 ++++++++++++---- README.rst | 1 + arrow/_version.py | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 41d6ca56b..db6addb45 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,12 @@ Changelog ========= -1.0.0 (2021-02-24) +1.0.1 (2021-02-27) +------------------ + +- [FIXED] A ``py.typed`` file is now bundled with the Arrow package to conform to PEP 561. + +1.0.0 (2021-02-26) ------------------ After 8 years we're pleased to announce Arrow v1.0. Thanks to the entire Python community for helping make Arrow the amazing package it is today! @@ -13,7 +18,8 @@ After 8 years we're pleased to announce Arrow v1.0. Thanks to the entire Python - [NEW] Added support for Python 3.9. - [NEW] Added a new keyword argument "exact" to ``span``, ``span_range`` and ``interval`` methods. This makes timespans begin at the start time given and not extend beyond the end time given, for example: -..code-block:: python +.. code-block:: python + >>> start = Arrow(2021, 2, 5, 12, 30) >>> end = Arrow(2021, 2, 5, 17, 15) >>> for r in arrow.Arrow.span_range('hour', start, end, exact=True): @@ -36,12 +42,14 @@ After 8 years we're pleased to announce Arrow v1.0. Thanks to the entire Python - [WARN] Arrow will **drop support** for Python 2.7 and 3.5 in the upcoming 1.0.0 release. This is the last major release to support Python 2.7 and Python 3.5. - [NEW] Arrow now properly handles imaginary datetimes during DST shifts. For example: -..code-block:: python +.. code-block:: python + >>> just_before = arrow.get(2013, 3, 31, 1, 55, tzinfo="Europe/Paris") >>> just_before.shift(minutes=+10) -..code-block:: python +.. code-block:: python + >>> before = arrow.get("2018-03-10 23:00:00", "YYYY-MM-DD HH:mm:ss", tzinfo="US/Pacific") >>> after = arrow.get("2018-03-11 04:00:00", "YYYY-MM-DD HH:mm:ss", tzinfo="US/Pacific") >>> result=[(t, t.to("utc")) for t in arrow.Arrow.range("hour", before, after)] diff --git a/README.rst b/README.rst index bae1ba019..5eaa2e7e7 100644 --- a/README.rst +++ b/README.rst @@ -58,6 +58,7 @@ Features - Generates time spans, ranges, floors and ceilings for time frames ranging from microsecond to year - Humanize dates and times with a growing list of contributed locales - Extensible for your own Arrow-derived types +- Full support for PEP 484-style type hints Quick Start ----------- diff --git a/arrow/_version.py b/arrow/_version.py index 5becc17c0..5c4105cd3 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "1.0.1" From d66cb9632dad08d0d9a2f23f5669ad7c2d0c2a21 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 27 Feb 2021 19:25:56 -0500 Subject: [PATCH 498/649] Remove timestamp property references in examples; replace with function where relevant. (#929) --- README.rst | 4 ++-- docs/index.rst | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 5eaa2e7e7..abdf75560 100644 --- a/README.rst +++ b/README.rst @@ -93,8 +93,8 @@ Example Usage >>> local - >>> local.timestamp - 1368303838 + >>> local.timestamp() + 1368303838.970460 >>> local.format() '2013-05-11 13:23:58 -07:00' diff --git a/docs/index.rst b/docs/index.rst index e2830b04f..42bd73fda 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -93,9 +93,6 @@ Get a datetime or timestamp representation: >>> a.datetime datetime.datetime(2013, 5, 7, 4, 38, 15, 447644, tzinfo=tzutc()) - >>> a.timestamp - 1367901495 - Get a naive datetime, and tzinfo: .. code-block:: python From 86116aea967b67471a9dfe8da5d0be8acaa147a3 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 28 Feb 2021 15:04:31 -0500 Subject: [PATCH 499/649] Fix overflow on 32-bit Linux systems (#931) --- arrow/constants.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/arrow/constants.py b/arrow/constants.py index 1bf36d6c3..22ffe55dc 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -15,14 +15,15 @@ # but will trigger an OverflowError, ValueError, or OSError on Windows _MAX_TIMESTAMP = datetime.max.timestamp() except (OverflowError, ValueError, OSError): # pragma: no cover - # Fallback for Windows if initial max timestamp call fails + # Fallback for Windows and 32-bit systems if initial max timestamp call fails # 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 _MAX_TIMESTAMP = ( - datetime(3000, 12, 31, 23, 59, 59, 999999).timestamp() + datetime(3000, 1, 1, 23, 59, 59, 999999).timestamp() if is_64bits - else datetime(2038, 1, 18, 23, 59, 59, 999999).timestamp() + else datetime(2038, 1, 1, 23, 59, 59, 999999).timestamp() ) MAX_TIMESTAMP: Final[float] = _MAX_TIMESTAMP From fc2cbaa31276766147aa0a921be7dc7ff51291db Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 28 Feb 2021 17:47:22 -0500 Subject: [PATCH 500/649] Bump version to 1.0.2 and update CHANGELOG (#932) * Fix overflow on 32-bit Linux systems * Prep v1.0.2 release --- CHANGELOG.rst | 5 +++++ arrow/_version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index db6addb45..20541f9e4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog ========= +1.0.2 (2021-02-28) +------------------ + +- [FIXED] Fixed an ``OverflowError`` that could occur when running Arrow on a 32-bit OS. + 1.0.1 (2021-02-27) ------------------ diff --git a/arrow/_version.py b/arrow/_version.py index 5c4105cd3..7863915fa 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "1.0.1" +__version__ = "1.0.2" From 47c715d9aa9b01aa6681f64e4c936070cc217944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ola=20Nystr=C3=B6m?= Date: Tue, 2 Mar 2021 20:54:51 +0100 Subject: [PATCH 501/649] Swedish locale fix (#935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update locales.py Removed "några" from seconds. That is not how we write it. * Added TestSwedishLocale * fix linting --- arrow/locales.py | 2 +- tests/test_locales.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index 73b57533c..d801f9d63 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -788,7 +788,7 @@ class SwedishLocale(Locale): timeframes = { "now": "just nu", "second": "en sekund", - "seconds": "{0} några sekunder", + "seconds": "{0} sekunder", "minute": "en minut", "minutes": "{0} minuter", "hour": "en timme", diff --git a/tests/test_locales.py b/tests/test_locales.py index 642013ba5..0731115dd 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1367,3 +1367,23 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("months", 11) == "11ヶ月" assert self.locale._format_timeframe("year", 1) == "1年" assert self.locale._format_timeframe("years", 12) == "12年" + + +@pytest.mark.usefixtures("lang_locale") +class TestSwedishLocale: + def test_plurals(self): + assert self.locale._format_timeframe("now", 0) == "just nu" + assert self.locale._format_timeframe("second", 1) == "en sekund" + assert self.locale._format_timeframe("seconds", 30) == "30 sekunder" + assert self.locale._format_timeframe("minute", 1) == "en minut" + assert self.locale._format_timeframe("minutes", 40) == "40 minuter" + assert self.locale._format_timeframe("hour", 1) == "en timme" + assert self.locale._format_timeframe("hours", 23) == "23 timmar" + assert self.locale._format_timeframe("day", 1) == "en dag" + assert self.locale._format_timeframe("days", 12) == "12 dagar" + assert self.locale._format_timeframe("week", 1) == "en vecka" + assert self.locale._format_timeframe("weeks", 38) == "38 veckor" + assert self.locale._format_timeframe("month", 1) == "en månad" + assert self.locale._format_timeframe("months", 11) == "11 månader" + assert self.locale._format_timeframe("year", 1) == "ett år" + assert self.locale._format_timeframe("years", 12) == "12 år" From 77b4c5dda714fe4ce264ea272cf93a7bf0f0cec7 Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Tue, 2 Mar 2021 20:51:13 +0000 Subject: [PATCH 502/649] Lower coverage requirement to 99% until humanize xfail tests are fixed (#937) Co-authored-by: Jad Chaar --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 4faae0236..a1ebbc93b 100644 --- a/tox.ini +++ b/tox.ini @@ -39,7 +39,7 @@ commands = make html SPHINXOPTS="-W --keep-going" [pytest] -addopts = -v --cov-branch --cov=arrow --cov-fail-under=100 --cov-report=term-missing --cov-report=xml +addopts = -v --cov-branch --cov=arrow --cov-fail-under=99 --cov-report=term-missing --cov-report=xml testpaths = tests [isort] From c577c76ee70415c0597e9ebd3c8e1d95bf63d04d Mon Sep 17 00:00:00 2001 From: Konstantin Weddige Date: Fri, 5 Mar 2021 18:32:36 +0100 Subject: [PATCH 503/649] Reconfigure arrow/__init__.py to avoid mypy --strict problems (#938) * Explicit reexport values Mypy prevents the implicit reexport, when --no-implicit-reexport is given. * Add missing ParserError and add comment * Fix typo in comment --- arrow/__init__.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/arrow/__init__.py b/arrow/__init__.py index 117c9e8a0..bc5970970 100644 --- a/arrow/__init__.py +++ b/arrow/__init__.py @@ -15,3 +15,25 @@ FORMAT_W3C, ) from .parser import ParserError + +# https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-no-implicit-reexport +# Mypy with --strict or --no-implicit-reexport requires an explicit reexport. +__all__ = [ + "__version__", + "get", + "now", + "utcnow", + "Arrow", + "ArrowFactory", + "FORMAT_ATOM", + "FORMAT_COOKIE", + "FORMAT_RFC822", + "FORMAT_RFC850", + "FORMAT_RFC1036", + "FORMAT_RFC1123", + "FORMAT_RFC2822", + "FORMAT_RFC3339", + "FORMAT_RSS", + "FORMAT_W3C", + "ParserError", +] From ce69c31f18a66f37897600f6c1ab86bc4cf256ea Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Sun, 7 Mar 2021 16:26:57 +0000 Subject: [PATCH 504/649] Bump version to 1.0.3 and update CHANGELOG (#940) --- CHANGELOG.rst | 7 +++++++ arrow/_version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 20541f9e4..300af2089 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Changelog ========= +1.0.3 (2021-03-05) +------------------ + +- [FIX] Updated internals to avoid issues when running ``mypy --strict``. +- [FIX] Corrections to Swedish locale. +- [INTERNAL] Lowered required coverage limit until ``humanize`` month tests are fixed. + 1.0.2 (2021-02-28) ------------------ diff --git a/arrow/_version.py b/arrow/_version.py index 7863915fa..976498ab9 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "1.0.2" +__version__ = "1.0.3" From cf8a0cc746c39cdc7109dbc91522429e82da3781 Mon Sep 17 00:00:00 2001 From: Ryan Kelly Date: Sun, 14 Mar 2021 15:30:38 -0400 Subject: [PATCH 505/649] Restore datetime constructor type acceptability in factory.get() (#943) * restore int, int, int type acceptability to .get * allow for a variable number of integer arguments a la datetime.datetime there does not seem to be a clear way to enforce 3-7 (the number of integer arguments accepted by datetime.datetime) without duplicating this definition for each case --- arrow/api.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/arrow/api.py b/arrow/api.py index 95696f3c1..3cf7868d6 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -29,6 +29,16 @@ def get( ... # pragma: no cover +@overload +def get( + *args: int, + locale: str = "en_us", + tzinfo: Optional[TZ_EXPR] = None, + normalize_whitespace: bool = False, +) -> Arrow: + ... # pragma: no cover + + @overload def get( __obj: Union[ From 45ad1129dce6de67aa0a6f3eaab04d2ee91d0428 Mon Sep 17 00:00:00 2001 From: cyriaka90 Date: Thu, 1 Apr 2021 14:34:15 +0200 Subject: [PATCH 506/649] Add Croatian locale --- arrow/locales.py | 94 +++++++++++++++++++++++++++++++++++++++++++ tests/test_locales.py | 27 +++++++++++++ 2 files changed, 121 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index d801f9d63..4253f3d04 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -4390,4 +4390,98 @@ class SwahiliLocale(Locale): ] +class CroatianLocale(Locale): + + names = ["hr", "hr-hr"] + + past = "prije {0}" + future = "za {0}" + and_word = "i" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "upravo sad", + "second": "sekundu", + "seconds": {"double": "{0} sekunde", "higher": "{0} sekundi"}, + "minute": "minutu", + "minutes": {"double": "{0} minute", "higher": "{0} minuta"}, + "hour": "sat", + "hours": {"double": "{0} sata", "higher": "{0} sati"}, + "day": "jedan dan", + "days": {"double": "{0} dana", "higher": "{0} dana"}, + "week": "tjedan", + "weeks": {"double": "{0} tjedna", "higher": "{0} tjedana"}, + "month": "mjesec", + "months": {"double": "{0} mjeseca", "higher": "{0} mjeseci"}, + "year": "godinu", + "years": {"double": "{0} godine", "higher": "{0} godina"}, + } + + month_names = [ + "", + "siječanj", + "veljača", + "ožujak", + "travanj", + "svibanj", + "lipanj", + "srpanj", + "kolovoz", + "rujan", + "listopad", + "studeni", + "prosinac", + ] + + month_abbreviations = [ + "", + "siječ", + "velj", + "ožuj", + "trav", + "svib", + "lip", + "srp", + "kol", + "ruj", + "list", + "stud", + "pros", + ] + + day_names = [ + "", + "ponedjeljak", + "utorak", + "srijeda", + "četvrtak", + "petak", + "subota", + "nedjelja", + ] + + day_abbreviations = [ + "", + "po", + "ut", + "sr", + "če", + "pe", + "su", + "ne", + ] + + def _format_timeframe( + self, timeframe: TimeFrameLiteral, delta: Union[float, int] + ) -> str: + form = self.timeframes[timeframe] + delta = abs(delta) + if isinstance(form, Mapping): + if 1 < delta <= 4: + form = form["double"] + else: + form = form["higher"] + + return form.format(delta) + + _locales: Dict[str, Type[Locale]] = _map_locales() diff --git a/tests/test_locales.py b/tests/test_locales.py index 0731115dd..0fc70e28d 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1114,6 +1114,33 @@ def test_ordinal_number_tl(self): assert self.locale.ordinal_number(114) == "ika-114" +@pytest.mark.usefixtures("lang_locale") +class TestCroatianLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "upravo sad" + assert self.locale._format_timeframe("second", 1) == "sekundu" + assert self.locale._format_timeframe("seconds", 3) == "3 sekunde" + assert self.locale._format_timeframe("seconds", 30) == "30 sekundi" + assert self.locale._format_timeframe("minute", 1) == "minutu" + assert self.locale._format_timeframe("minutes", 4) == "4 minute" + assert self.locale._format_timeframe("minutes", 40) == "40 minuta" + assert self.locale._format_timeframe("hour", 1) == "sat" + assert self.locale._format_timeframe("hours", 23) == "23 sati" + assert self.locale._format_timeframe("day", 1) == "jedan dan" + assert self.locale._format_timeframe("days", 12) == "12 dana" + assert self.locale._format_timeframe("month", 1) == "mjesec" + assert self.locale._format_timeframe("months", 2) == "2 mjeseca" + assert self.locale._format_timeframe("months", 11) == "11 mjeseci" + assert self.locale._format_timeframe("year", 1) == "godinu" + assert self.locale._format_timeframe("years", 2) == "2 godine" + assert self.locale._format_timeframe("years", 12) == "12 godina" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "subota" + assert self.locale.day_abbreviation(dt.isoweekday()) == "su" + + @pytest.mark.usefixtures("lang_locale") class TestEstonianLocale: def test_format_timeframe(self): From 85964d0d4eb3a957826fc60c6f992984e4411f2f Mon Sep 17 00:00:00 2001 From: cyriaka90 Date: Thu, 8 Apr 2021 01:09:43 +0200 Subject: [PATCH 507/649] Add Lithuanian (#950) --- arrow/locales.py | 81 +++++++++++++++++++++++++++++++++++++++++++ tests/test_locales.py | 26 ++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 4253f3d04..882776a00 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -4484,4 +4484,85 @@ def _format_timeframe( return form.format(delta) +class LithuanianLocale(Locale): + + names = ["lt", "lt-lt"] + + past = "prieš {0}" + future = "po {0}" + and_word = "ir" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "dabar", + "second": "sekundės", + "seconds": "{0} sekundžių", + "minute": "minutės", + "minutes": "{0} minučių", + "hour": "valandos", + "hours": "{0} valandų", + "day": "dieną", + "days": "{0} dienų", + "week": "savaitės", + "weeks": "{0} savaičių", + "month": "mėnesio", + "months": "{0} mėnesių", + "year": "metų", + "years": "{0} metų", + } + + month_names = [ + "", + "sausis", + "vasaris", + "kovas", + "balandis", + "gegužė", + "birželis", + "liepa", + "rugpjūtis", + "rugsėjis", + "spalis", + "lapkritis", + "gruodis", + ] + + month_abbreviations = [ + "", + "saus", + "vas", + "kovas", + "bal", + "geg", + "birž", + "liepa", + "rugp", + "rugs", + "spalis", + "lapkr", + "gr", + ] + + day_names = [ + "", + "pirmadienis", + "antradienis", + "trečiadienis", + "ketvirtadienis", + "penktadienis", + "šeštadienis", + "sekmadienis", + ] + + day_abbreviations = [ + "", + "pi", + "an", + "tr", + "ke", + "pe", + "še", + "se", + ] + + _locales: Dict[str, Type[Locale]] = _map_locales() diff --git a/tests/test_locales.py b/tests/test_locales.py index 0fc70e28d..c7e06bb21 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1141,6 +1141,32 @@ def test_weekday(self): assert self.locale.day_abbreviation(dt.isoweekday()) == "su" +@pytest.mark.usefixtures("lang_locale") +class TestLithuanianLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "dabar" + assert self.locale._format_timeframe("second", 1) == "sekundės" + assert self.locale._format_timeframe("seconds", 3) == "3 sekundžių" + assert self.locale._format_timeframe("seconds", 30) == "30 sekundžių" + assert self.locale._format_timeframe("minute", 1) == "minutės" + assert self.locale._format_timeframe("minutes", 4) == "4 minučių" + assert self.locale._format_timeframe("minutes", 40) == "40 minučių" + assert self.locale._format_timeframe("hour", 1) == "valandos" + assert self.locale._format_timeframe("hours", 23) == "23 valandų" + assert self.locale._format_timeframe("day", 1) == "dieną" + assert self.locale._format_timeframe("days", 12) == "12 dienų" + assert self.locale._format_timeframe("month", 1) == "mėnesio" + assert self.locale._format_timeframe("months", 2) == "2 mėnesių" + assert self.locale._format_timeframe("months", 11) == "11 mėnesių" + assert self.locale._format_timeframe("year", 1) == "metų" + assert self.locale._format_timeframe("years", 2) == "2 metų" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "šeštadienis" + assert self.locale.day_abbreviation(dt.isoweekday()) == "še" + + @pytest.mark.usefixtures("lang_locale") class TestEstonianLocale: def test_format_timeframe(self): From 485d40bac87c97b2ae86dd5b3ab85f62353657e0 Mon Sep 17 00:00:00 2001 From: cyriaka90 Date: Thu, 8 Apr 2021 01:25:01 +0200 Subject: [PATCH 508/649] Add Latvian locale (#951) Co-authored-by: Jad Chaar --- arrow/locales.py | 81 +++++++++++++++++++++++++++++++++++++++++++ tests/test_locales.py | 27 +++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 882776a00..8ceba33e2 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -4305,6 +4305,87 @@ def _format_timeframe( return _form.format(abs(delta)) +class LatvianLocale(Locale): + + names = ["lv", "lv-lv"] + + past = "pirms {0}" + future = "pēc {0}" + and_word = "un" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "tagad", + "second": "sekundes", + "seconds": "{0} sekundēm", + "minute": "minūtes", + "minutes": "{0} minūtēm", + "hour": "stundas", + "hours": "{0} stundām", + "day": "dienas", + "days": "{0} dienām", + "week": "nedēļas", + "weeks": "{0} nedēļām", + "month": "mēneša", + "months": "{0} mēnešiem", + "year": "gada", + "years": "{0} gadiem", + } + + month_names = [ + "", + "janvāris", + "februāris", + "marts", + "aprīlis", + "maijs", + "jūnijs", + "jūlijs", + "augusts", + "septembris", + "oktobris", + "novembris", + "decembris", + ] + + month_abbreviations = [ + "", + "jan", + "feb", + "marts", + "apr", + "maijs", + "jūnijs", + "jūlijs", + "aug", + "sept", + "okt", + "nov", + "dec", + ] + + day_names = [ + "", + "pirmdiena", + "otrdiena", + "trešdiena", + "ceturtdiena", + "piektdiena", + "sestdiena", + "svētdiena", + ] + + day_abbreviations = [ + "", + "pi", + "ot", + "tr", + "ce", + "pi", + "se", + "sv", + ] + + class SwahiliLocale(Locale): names = [ diff --git a/tests/test_locales.py b/tests/test_locales.py index c7e06bb21..721d22bb1 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1229,6 +1229,33 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("years", 12) == "12 anos" +@pytest.mark.usefixtures("lang_locale") +class TestLatvianLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "tagad" + assert self.locale._format_timeframe("second", 1) == "sekundes" + assert self.locale._format_timeframe("seconds", 3) == "3 sekundēm" + assert self.locale._format_timeframe("seconds", 30) == "30 sekundēm" + assert self.locale._format_timeframe("minute", 1) == "minūtes" + assert self.locale._format_timeframe("minutes", 4) == "4 minūtēm" + assert self.locale._format_timeframe("minutes", 40) == "40 minūtēm" + assert self.locale._format_timeframe("hour", 1) == "stundas" + assert self.locale._format_timeframe("hours", 23) == "23 stundām" + assert self.locale._format_timeframe("day", 1) == "dienas" + assert self.locale._format_timeframe("days", 12) == "12 dienām" + assert self.locale._format_timeframe("month", 1) == "mēneša" + assert self.locale._format_timeframe("months", 2) == "2 mēnešiem" + assert self.locale._format_timeframe("months", 11) == "11 mēnešiem" + assert self.locale._format_timeframe("year", 1) == "gada" + assert self.locale._format_timeframe("years", 2) == "2 gadiem" + assert self.locale._format_timeframe("years", 12) == "12 gadiem" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "sestdiena" + assert self.locale.day_abbreviation(dt.isoweekday()) == "se" + + @pytest.mark.usefixtures("lang_locale") class TestBrazilianPortugueseLocale: def test_format_timeframe(self): From 6941e32dbb9114109e935ab190115ca6d47c9a27 Mon Sep 17 00:00:00 2001 From: Isac Byeonghoon Yoo Date: Thu, 8 Apr 2021 13:48:27 +0900 Subject: [PATCH 509/649] Utilize __init_subclass__ instead of inspect on Locale mapping to improve type checking (#920) * Utilize __init_subclass__ * Add Unit Test for duplicated locale name Co-authored-by: Jad Chaar --- arrow/locales.py | 28 +++++++++++----------------- tests/conftest.py | 2 +- tests/test_locales.py | 10 ++++++++-- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 8ceba33e2..90ca2b5c7 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1,4 +1,3 @@ -import inspect import sys from math import trunc from typing import ( @@ -48,6 +47,9 @@ ] +_locale_map: Dict[str, Type["Locale"]] = dict() + + def get_locale(name: str) -> "Locale": """Returns an appropriate :class:`Locale ` corresponding to an input locale name. @@ -56,7 +58,7 @@ def get_locale(name: str) -> "Locale": """ - locale_cls = _locales.get(name.lower()) + locale_cls = _locale_map.get(name.lower()) if locale_cls is None: raise ValueError(f"Unsupported locale {name!r}.") @@ -121,6 +123,13 @@ class Locale: _month_name_to_ordinal: Optional[Dict[str, int]] + def __init_subclass__(cls, **kwargs: Any) -> None: + for locale_name in cls.names: + if locale_name in _locale_map: + raise LookupError(f"Duplicated locale name: {locale_name}") + + _locale_map[locale_name] = cls + def __init__(self) -> None: self._month_name_to_ordinal = None @@ -3409,18 +3418,6 @@ class MarathiLocale(Locale): day_abbreviations = ["", "सोम", "मंगळ", "बुध", "गुरु", "शुक्र", "शनि", "रवि"] -def _map_locales() -> Dict[str, Type[Locale]]: - - locales: Dict[str, Type[Locale]] = {} - - for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): - if issubclass(cls, Locale): # pragma: no branch - for name in cls.names: - locales[name.lower()] = cls - - return locales - - class CatalanLocale(Locale): names = ["ca", "ca_es", "ca_ad", "ca_fr", "ca_it"] past = "Fa {0}" @@ -4644,6 +4641,3 @@ class LithuanianLocale(Locale): "še", "se", ] - - -_locales: Dict[str, Type[Locale]] = _map_locales() diff --git a/tests/conftest.py b/tests/conftest.py index 4043bc3b1..5d5b9980e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,7 +49,7 @@ def arrow_factory(request): @pytest.fixture(scope="class") def lang_locales(request): - request.cls.locales = locales._locales + request.cls.locales = locales._locale_map @pytest.fixture(scope="class") diff --git a/tests/test_locales.py b/tests/test_locales.py index 721d22bb1..6c984ecd6 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -33,6 +33,12 @@ def test_locale_validation(self): assert locale_cls.past is not None assert locale_cls.future is not None + def test_duplicated_locale_name(self): + with pytest.raises(LookupError): + + class Locale1(locales.Locale): + names = ["en_us"] + class TestModule: def test_get_locale(self, mocker): @@ -43,7 +49,7 @@ def test_get_locale(self, mocker): with pytest.raises(ValueError): arrow.locales.get_locale("locale_name") - cls_dict = arrow.locales._locales + cls_dict = arrow.locales._locale_map mocker.patch.dict(cls_dict, {"locale_name": mock_locale_cls}) result = arrow.locales.get_locale("locale_name") @@ -68,7 +74,7 @@ def test_get_locale_by_class_name(self, mocker): def test_locales(self): - assert len(locales._locales) > 0 + assert len(locales._locale_map) > 0 @pytest.mark.usefixtures("lang_locale") From 57c67d02f244a5f267329d4510ae0bb44bfd936d Mon Sep 17 00:00:00 2001 From: cyriaka90 Date: Thu, 8 Apr 2021 17:17:33 +0200 Subject: [PATCH 510/649] Add Latin locale (#955) --- arrow/locales.py | 81 +++++++++++++++++++++++++++++++++++++++++++ tests/test_locales.py | 22 ++++++++++++ 2 files changed, 103 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 90ca2b5c7..5942bd85f 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -4562,6 +4562,87 @@ def _format_timeframe( return form.format(delta) +class LatinLocale(Locale): + + names = ["la", "la-va"] + + past = "ante {0}" + future = "in {0}" + and_word = "et" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "nunc", + "second": "secundum", + "seconds": "{0} secundis", + "minute": "minutam", + "minutes": "{0} minutis", + "hour": "horam", + "hours": "{0} horas", + "day": "diem", + "days": "{0} dies", + "week": "hebdomadem", + "weeks": "{0} hebdomades", + "month": "mensem", + "months": "{0} mensis", + "year": "annum", + "years": "{0} annos", + } + + month_names = [ + "", + "Ianuarius", + "Februarius", + "Martius", + "Aprilis", + "Maius", + "Iunius", + "Iulius", + "Augustus", + "September", + "October", + "November", + "December", + ] + + month_abbreviations = [ + "", + "Ian", + "Febr", + "Mart", + "Apr", + "Mai", + "Iun", + "Iul", + "Aug", + "Sept", + "Oct", + "Nov", + "Dec", + ] + + day_names = [ + "", + "dies Lunae", + "dies Martis", + "dies Mercurii", + "dies Iovis", + "dies Veneris", + "dies Saturni", + "dies Solis", + ] + + day_abbreviations = [ + "", + "dies Lunae", + "dies Martis", + "dies Mercurii", + "dies Iovis", + "dies Veneris", + "dies Saturni", + "dies Solis", + ] + + class LithuanianLocale(Locale): names = ["lt", "lt-lt"] diff --git a/tests/test_locales.py b/tests/test_locales.py index 6c984ecd6..5f2bad03d 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1147,6 +1147,28 @@ def test_weekday(self): assert self.locale.day_abbreviation(dt.isoweekday()) == "su" +@pytest.mark.usefixtures("lang_locale") +class TestLatinLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "nunc" + assert self.locale._format_timeframe("second", 1) == "secundum" + assert self.locale._format_timeframe("seconds", 3) == "3 secundis" + assert self.locale._format_timeframe("minute", 1) == "minutam" + assert self.locale._format_timeframe("minutes", 4) == "4 minutis" + assert self.locale._format_timeframe("hour", 1) == "horam" + assert self.locale._format_timeframe("hours", 23) == "23 horas" + assert self.locale._format_timeframe("day", 1) == "diem" + assert self.locale._format_timeframe("days", 12) == "12 dies" + assert self.locale._format_timeframe("month", 1) == "mensem" + assert self.locale._format_timeframe("months", 11) == "11 mensis" + assert self.locale._format_timeframe("year", 1) == "annum" + assert self.locale._format_timeframe("years", 2) == "2 annos" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "dies Saturni" + + @pytest.mark.usefixtures("lang_locale") class TestLithuanianLocale: def test_format_timeframe(self): From f18be7fdf03060dc17884cfd5ed9d789d51092e6 Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Sun, 11 Apr 2021 19:35:35 +0100 Subject: [PATCH 511/649] Audit all docstrings for style, typos and outdated info (#939) * Start working through docstrings * Further work on module docstrings * Further work on docstrings * Continue review of arrow.py and fix #946 * Finish off arrow.py * Remove comment * Suggested changes * New example for fromdatetime() * Minor changes Co-authored-by: Jad Chaar --- arrow/arrow.py | 55 +++++++++++++++++++++++++++------------------- arrow/constants.py | 2 ++ arrow/factory.py | 7 +----- arrow/formatter.py | 2 ++ arrow/locales.py | 2 ++ arrow/parser.py | 2 ++ arrow/util.py | 16 ++++++++++++-- 7 files changed, 56 insertions(+), 30 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index dda7b4056..90257bb34 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -91,7 +91,7 @@ class Arrow: :param second: (optional) the second, Defaults to 0. :param microsecond: (optional) the microsecond. Defaults to 0. :param tzinfo: (optional) A timezone expression. Defaults to UTC. - :param fold: (optional) 0 or 1, used to disambiguate repeated times. Defaults to 0. + :param fold: (optional) 0 or 1, used to disambiguate repeated wall times. Defaults to 0. .. _tz-expr: @@ -245,6 +245,7 @@ def fromtimestamp( :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. + """ if tzinfo is None: @@ -305,12 +306,12 @@ def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arr :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to ``dt``'s timezone, or UTC if naive. - If you only want to replace the timezone of naive datetimes:: + Usage:: >>> dt - datetime.datetime(2013, 5, 5, 0, 0, tzinfo=tzutc()) - >>> arrow.Arrow.fromdatetime(dt, dt.tzinfo or 'US/Pacific') - + datetime.datetime(2021, 4, 7, 13, 48, tzinfo=tzfile('/usr/share/zoneinfo/US/Pacific')) + >>> arrow.Arrow.fromdatetime(dt) + """ @@ -335,10 +336,11 @@ def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arr @classmethod def fromdate(cls, date: date, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": """Constructs an :class:`Arrow ` object from a ``date`` and optional - replacement timezone. Time values are set to 0. + replacement timezone. All time values are set to 0. :param date: the ``date`` :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to UTC. + """ if tzinfo is None: @@ -354,7 +356,7 @@ def strptime( in the style of ``datetime.strptime``. Optionally replaces the parsed timezone. :param date_str: the date string. - :param fmt: the format string. + :param fmt: the format string using datetime format codes. :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to the parsed timezone if ``fmt`` contains a timezone directive, otherwise UTC. @@ -438,7 +440,7 @@ def range( iterating. As such, either call with naive objects and ``tz``, or aware objects from the same timezone and no ``tz``. - Supported frame values: year, quarter, month, week, day, hour, minute, second. + Supported frame values: year, quarter, month, week, day, hour, minute, second, microsecond. Recognized datetime expressions: @@ -505,7 +507,7 @@ def span( bounds: _BOUNDS = "[)", exact: bool = False, ) -> Tuple["Arrow", "Arrow"]: - """Returns two new :class:`Arrow ` objects, representing the timespan + """Returns a tuple of two new :class:`Arrow ` objects, representing the timespan of the :class:`Arrow ` object in a given timeframe. :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). @@ -589,6 +591,7 @@ def floor(self, frame: _T_FRAMES) -> "Arrow": >>> arrow.utcnow().floor('hour') + """ return self.span(frame)[0] @@ -605,6 +608,7 @@ def ceil(self, frame: _T_FRAMES) -> "Arrow": >>> arrow.utcnow().ceil('hour') + """ return self.span(frame)[1] @@ -645,7 +649,7 @@ def span_range( iterating. As such, either call with naive objects and ``tz``, or aware objects from the same timezone and no ``tz``. - Supported frame values: year, quarter, month, week, day, hour, minute, second. + Supported frame values: year, quarter, month, week, day, hour, minute, second, microsecond. Recognized datetime expressions: @@ -849,8 +853,8 @@ def timestamp(self) -> float: Usage:: - >>> arrow.utcnow().timestamp - 1548260567 + >>> arrow.utcnow().timestamp() + 1616882340.256501 """ @@ -858,7 +862,7 @@ def timestamp(self) -> float: @property def int_timestamp(self) -> int: - """Returns a timestamp representation of the :class:`Arrow ` object, in + """Returns an integer timestamp representation of the :class:`Arrow ` object, in UTC time. Usage:: @@ -872,7 +876,7 @@ def int_timestamp(self) -> int: @property def float_timestamp(self) -> float: - """Returns a floating-point representation of the :class:`Arrow ` + """Returns a floating-point timestamp representation of the :class:`Arrow ` object, in UTC time. Usage:: @@ -892,7 +896,10 @@ def fold(self) -> int: @property def ambiguous(self) -> bool: - """ Returns a boolean indicating whether the :class:`Arrow ` object is ambiguous.""" + """Indicates whether the :class:`Arrow ` object is a repeated wall time in the current + timezone. + + """ return dateutil_tz.datetime_ambiguous(self._datetime) @@ -1069,7 +1076,7 @@ def to(self, tz: TZ_EXPR) -> "Arrow": def format(self, fmt: str = "YYYY-MM-DD HH:mm:ssZZ", locale: str = "en_us") -> str: """Returns a string representation of the :class:`Arrow ` object, - formatted according to a format string. + formatted according to the provided format string. :param fmt: the format string. :param locale: the locale to format. @@ -1283,8 +1290,8 @@ def is_between( end: "Arrow", bounds: _BOUNDS = "()", ) -> bool: - """Returns a boolean denoting whether the specified date and time is between - the start and end dates and times. + """Returns a boolean denoting whether the :class:`Arrow ` object is between + the start and end limits. :param start: an :class:`Arrow ` object. :param end: an :class:`Arrow ` object. @@ -1622,10 +1629,9 @@ def __le__(self, other: Any) -> bool: return self._datetime <= self._get_datetime(other) # internal methods - @staticmethod 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() if isinstance(tz_expr, dt_tzinfo): @@ -1640,7 +1646,7 @@ def _get_tzinfo(tz_expr: Optional[TZ_EXPR]) -> dt_tzinfo: def _get_datetime( cls, expr: Union["Arrow", dt_datetime, int, float, str] ) -> dt_datetime: - """Get datetime object for a specified expression.""" + """Get datetime object from a specified expression.""" if isinstance(expr, Arrow): return expr.datetime elif isinstance(expr, dt_datetime): @@ -1653,7 +1659,11 @@ def _get_datetime( @classmethod def _get_frames(cls, name: _T_FRAMES) -> Tuple[str, str, int]: + """Finds relevant timeframe and steps for use in range and span methods. + + Returns a 3 element tuple in the form (frame, plural frame, step), for example ("day", "days", 1) + """ if name in cls._ATTRS: return name, f"{name}s", 1 elif name[-1] == "s" and name[:-1] in cls._ATTRS: @@ -1682,7 +1692,7 @@ def _get_frames(cls, name: _T_FRAMES) -> Tuple[str, str, int]: @classmethod 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: @@ -1697,6 +1707,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] diff --git a/arrow/constants.py b/arrow/constants.py index 22ffe55dc..f59b50183 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -1,3 +1,5 @@ +"""Constants used internally in arrow.""" + import sys from datetime import datetime diff --git a/arrow/factory.py b/arrow/factory.py index 231510f58..1484796eb 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -107,11 +107,6 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: >>> arrow.get() - **None** to also get current UTC time:: - - >>> arrow.get(None) - - **One** :class:`Arrow ` object, to get a copy. >>> arw = arrow.utcnow() @@ -189,7 +184,7 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: >>> arrow.get('2013-05-05 12:30:45', ['MM/DD/YYYY', 'YYYY-MM-DD HH:mm:ss']) - **Three or more** arguments, as for the constructor of a ``datetime``:: + **Three or more** arguments, as for the direct constructor of an ``Arrow`` object:: >>> arrow.get(2013, 5, 5, 12, 30, 45) diff --git a/arrow/formatter.py b/arrow/formatter.py index 14eb44a20..1744faec4 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -1,3 +1,5 @@ +"""Provides the :class:`Arrow ` class, an improved formatter for datetimes.""" + import re import sys from datetime import datetime, timedelta diff --git a/arrow/locales.py b/arrow/locales.py index 5942bd85f..e00a63c7c 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1,3 +1,5 @@ +"""Provides internationalization for arrow in over 60 languages and dialects.""" + import sys from math import trunc from typing import ( diff --git a/arrow/parser.py b/arrow/parser.py index a6a617dbc..ca0eac18c 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -1,3 +1,5 @@ +"""Provides the :class:`Arrow ` class, a better way to parse datetime strings.""" + import re import sys from datetime import datetime, timedelta diff --git a/arrow/util.py b/arrow/util.py index 8679131ee..f3eaa21c9 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -1,3 +1,5 @@ +"""Helpful functions used internally within arrow.""" + import datetime from typing import Any, Optional, cast @@ -57,7 +59,11 @@ def is_timestamp(value: Any) -> bool: def validate_ordinal(value: Any) -> None: - """Raise the corresponding exception if value is an invalid Gregorian ordinal.""" + """Raise an exception if value is an invalid Gregorian ordinal. + + :param value: the input to be checked + + """ if isinstance(value, bool) or not isinstance(value, int): raise TypeError(f"Ordinal must be an integer (got type {type(value)}).") if not (MIN_ORDINAL <= value <= MAX_ORDINAL): @@ -78,7 +84,13 @@ def normalize_timestamp(timestamp: float) -> float: # Credit to https://stackoverflow.com/a/1700069 def iso_to_gregorian(iso_year: int, iso_week: int, iso_day: int) -> datetime.date: - """Converts an ISO week date tuple into a datetime object.""" + """Converts an ISO week date into a datetime object. + + :param iso_year: the year + :param iso_week: the week number, each year has either 52 or 53 weeks + :param iso_day: the day numbered 1 through 7, beginning with Monday + + """ if not 1 <= iso_week <= 53: raise ValueError("ISO Calendar week value must be between 1-53.") From 60bc1960b020cd9867a2b2b4f7790b051566d368 Mon Sep 17 00:00:00 2001 From: ALee008 <39236016+ALee008@users.noreply.github.com> Date: Thu, 15 Apr 2021 03:07:21 +0200 Subject: [PATCH 512/649] Make start of week adjustable when using span (#934) * added parameter weekday and thus ability to choose start day when using span in combination with frame == "week" * changed default value for new parameter weekday. * added test for new parameter weekday when using span and frame == "week" * added comma after new parameter weekday. * Update arrow/arrow.py Co-authored-by: Chris <30196510+systemcatch@users.noreply.github.com> * changed parameter name from weekday to week_start. * extended doc string to highlight new parameter week_start. * raise ValueError if 1 <= week_start <= 7 is not met * renamed weekday to week_start in comment. * changed if-else block to one liner. Updated comments. * Update arrow/arrow.py add blank line at the end of the docstring Co-authored-by: Chris <30196510+systemcatch@users.noreply.github.com> * Remove trailing whitespace * added new line at end of doc string. * Fix doc issue * extended test_span_week by further test cases. Co-authored-by: Jad Chaar Co-authored-by: Chris <30196510+systemcatch@users.noreply.github.com> Co-authored-by: Jad Chaar --- arrow/arrow.py | 15 ++++++++++++++- tests/test_arrow.py | 23 ++++++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 90257bb34..ccd42bcec 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -506,6 +506,7 @@ def span( count: int = 1, bounds: _BOUNDS = "[)", exact: bool = False, + week_start: int = 1, ) -> Tuple["Arrow", "Arrow"]: """Returns a tuple of two new :class:`Arrow ` objects, representing the timespan of the :class:`Arrow ` object in a given timeframe. @@ -519,6 +520,8 @@ def span( :param exact: (optional) whether to have the start of the timespan begin exactly at the time specified by ``start`` and the end of the timespan truncated so as not to extend beyond ``end``. + :param week_start: (optional) only used in combination with the week timeframe. Follows isoweekday() where + Monday is 1 and Sunday is 7. Supported frame values: year, quarter, month, week, day, hour, minute, second. @@ -539,7 +542,15 @@ def span( >>> arrow.utcnow().span('day', bounds='[]') (, ) + >>> arrow.utcnow().span('week') + (, ) + + >>> arrow.utcnow().span('week', week_start=6) + (, ) + """ + if not 1 <= week_start <= 7: + raise ValueError("week_start argument must be between 1 and 7.") util.validate_bounds(bounds) @@ -565,7 +576,9 @@ def span( floor = self.__class__(*values, tzinfo=self.tzinfo) # type: ignore if frame_absolute == "week": - floor = floor.shift(days=-(self.isoweekday() - 1)) + # if week_start is greater than self.isoweekday() go back one week by setting delta = 7 + delta = 7 if week_start > self.isoweekday() else 0 + floor = floor.shift(days=-(self.isoweekday() - week_start) - delta) elif frame_absolute == "quarter": floor = floor.shift(months=-((self.month - 1) % 3)) diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 2132040ca..473af36b5 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -1748,11 +1748,30 @@ def test_span_month(self): assert ceil == datetime(2013, 2, 28, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_week(self): - + """ + >>> self.arrow.format("YYYY-MM-DD") == "2013-02-15" + >>> self.arrow.isoweekday() == 5 # a Friday + """ + # span week from Monday to Sunday floor, ceil = self.arrow.span("week") assert floor == datetime(2013, 2, 11, tzinfo=tz.tzutc()) assert ceil == datetime(2013, 2, 17, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + # span week from Tuesday to Monday + floor, ceil = self.arrow.span("week", week_start=2) + + assert floor == datetime(2013, 2, 12, tzinfo=tz.tzutc()) + assert ceil == datetime(2013, 2, 18, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + # span week from Saturday to Friday + floor, ceil = self.arrow.span("week", week_start=6) + + assert floor == datetime(2013, 2, 9, tzinfo=tz.tzutc()) + assert ceil == datetime(2013, 2, 15, 23, 59, 59, 999999, tzinfo=tz.tzutc()) + # span week from Sunday to Saturday + floor, ceil = self.arrow.span("week", week_start=7) + + assert floor == datetime(2013, 2, 10, tzinfo=tz.tzutc()) + assert ceil == datetime(2013, 2, 16, 23, 59, 59, 999999, tzinfo=tz.tzutc()) def test_span_day(self): @@ -2347,6 +2366,8 @@ def test_value_error_exception(self): target.is_between(start, end, "[") with pytest.raises(ValueError): target.is_between(start, end, "hello") + with pytest.raises(ValueError): + target.span("week", week_start=55) class TestArrowUtil: From ac6a204e2a2c969103ad2415b641b4d1cc4092bb Mon Sep 17 00:00:00 2001 From: cyriaka90 Date: Thu, 15 Apr 2021 19:11:51 +0200 Subject: [PATCH 513/649] Add Malay locale (#959) --- arrow/locales.py | 81 +++++++++++++++++++++++++++++++++++++++++++ tests/test_locales.py | 22 ++++++++++++ 2 files changed, 103 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index e00a63c7c..328050e64 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -4724,3 +4724,84 @@ class LithuanianLocale(Locale): "še", "se", ] + + +class MalayLocale(Locale): + + names = ["ms", "ms-my", "ms-bn"] + + past = "{0} yang lalu" + future = "dalam {0}" + and_word = "dan" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "sekarang", + "second": "saat", + "seconds": "{0} saat", + "minute": "minit", + "minutes": "{0} minit", + "hour": "jam", + "hours": "{0} jam", + "day": "hari", + "days": "{0} hari", + "week": "minggu", + "weeks": "{0} minggu", + "month": "bulan", + "months": "{0} bulan", + "year": "tahun", + "years": "{0} tahun", + } + + month_names = [ + "", + "Januari", + "Februari", + "Mac", + "April", + "Mei", + "Jun", + "Julai", + "Ogos", + "September", + "Oktober", + "November", + "Disember", + ] + + month_abbreviations = [ + "", + "Jan.", + "Feb.", + "Mac", + "Apr.", + "Mei", + "Jun", + "Julai", + "Og.", + "Sept.", + "Okt.", + "Nov.", + "Dis.", + ] + + day_names = [ + "", + "Isnin", + "Selasa", + "Rabu", + "Khamis", + "Jumaat", + "Sabtu", + "Ahad ", + ] + + day_abbreviations = [ + "", + "Isnin", + "Selasa", + "Rabu", + "Khamis", + "Jumaat", + "Sabtu", + "Ahad ", + ] diff --git a/tests/test_locales.py b/tests/test_locales.py index 5f2bad03d..7445a883d 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1195,6 +1195,28 @@ def test_weekday(self): assert self.locale.day_abbreviation(dt.isoweekday()) == "še" +@pytest.mark.usefixtures("lang_locale") +class TestMalayLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "sekarang" + assert self.locale._format_timeframe("second", 1) == "saat" + assert self.locale._format_timeframe("seconds", 3) == "3 saat" + assert self.locale._format_timeframe("minute", 1) == "minit" + assert self.locale._format_timeframe("minutes", 4) == "4 minit" + assert self.locale._format_timeframe("hour", 1) == "jam" + assert self.locale._format_timeframe("hours", 23) == "23 jam" + assert self.locale._format_timeframe("day", 1) == "hari" + assert self.locale._format_timeframe("days", 12) == "12 hari" + assert self.locale._format_timeframe("month", 1) == "bulan" + assert self.locale._format_timeframe("months", 2) == "2 bulan" + assert self.locale._format_timeframe("year", 1) == "tahun" + assert self.locale._format_timeframe("years", 2) == "2 tahun" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "Sabtu" + + @pytest.mark.usefixtures("lang_locale") class TestEstonianLocale: def test_format_timeframe(self): From 1310dbbc32ce41adac4840e4e399da0f77836041 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 18 Apr 2021 13:29:43 -0400 Subject: [PATCH 514/649] Standardize locales and improve locale validation (#954) * Progress toward locale validation * standardize and validate locales * Add tests * Clean up tests * Use new locale name in error * Remove useless comments * Address comments * English * Remove default locale from test_parser --- README.rst | 4 +- arrow/api.py | 11 +-- arrow/arrow.py | 9 ++- arrow/constants.py | 2 + arrow/factory.py | 13 ++-- arrow/formatter.py | 3 +- arrow/locales.py | 171 ++++++++++++++++++++---------------------- arrow/parser.py | 3 +- tests/test_locales.py | 40 +++++++++- tests/test_parser.py | 22 +++--- 10 files changed, 157 insertions(+), 121 deletions(-) diff --git a/README.rst b/README.rst index abdf75560..69f91fe5c 100644 --- a/README.rst +++ b/README.rst @@ -105,8 +105,8 @@ Example Usage >>> local.humanize() 'an hour ago' - >>> local.humanize(locale='ko_kr') - '1시간 전' + >>> local.humanize(locale='ko-kr') + '한시간 전' .. end-inclusion-marker-do-not-remove diff --git a/arrow/api.py b/arrow/api.py index 3cf7868d6..d8ed24b97 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -10,6 +10,7 @@ from typing import Any, List, Optional, Tuple, Type, Union, overload from arrow.arrow import TZ_EXPR, Arrow +from arrow.constants import DEFAULT_LOCALE from arrow.factory import ArrowFactory # internal default factory. @@ -22,7 +23,7 @@ @overload def get( *, - locale: str = "en_us", + locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, ) -> Arrow: @@ -32,7 +33,7 @@ def get( @overload def get( *args: int, - locale: str = "en_us", + locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, ) -> Arrow: @@ -53,7 +54,7 @@ def get( Tuple[int, int, int], ], *, - locale: str = "en_us", + locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, ) -> Arrow: @@ -65,7 +66,7 @@ def get( __arg1: Union[datetime, date], __arg2: TZ_EXPR, *, - locale: str = "en_us", + locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, ) -> Arrow: @@ -77,7 +78,7 @@ def get( __arg1: str, __arg2: Union[str, List[str]], *, - locale: str = "en_us", + locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, ) -> Arrow: diff --git a/arrow/arrow.py b/arrow/arrow.py index ccd42bcec..0de43889d 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -32,6 +32,7 @@ from dateutil.relativedelta import relativedelta from arrow import formatter, locales, parser, util +from arrow.constants import DEFAULT_LOCALE from arrow.locales import TimeFrameLiteral if sys.version_info < (3, 8): # pragma: no cover @@ -1087,7 +1088,9 @@ def to(self, tz: TZ_EXPR) -> "Arrow": # string output and formatting - def format(self, fmt: str = "YYYY-MM-DD HH:mm:ssZZ", locale: str = "en_us") -> str: + 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. @@ -1115,7 +1118,7 @@ def format(self, fmt: str = "YYYY-MM-DD HH:mm:ssZZ", locale: str = "en_us") -> s def humanize( self, other: Union["Arrow", dt_datetime, None] = None, - locale: str = "en_us", + locale: str = DEFAULT_LOCALE, only_distance: bool = False, granularity: Union[_GRANULARITY, List[_GRANULARITY]] = "auto", ) -> str: @@ -1123,7 +1126,7 @@ def humanize( :param other: (optional) an :class:`Arrow ` or ``datetime`` object. Defaults to now in the current :class:`Arrow ` object's timezone. - :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'. + :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en-us'. :param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part. :param granularity: (optional) defines the precision of the output. Set it to strings 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year' or a list of any combination of these strings diff --git a/arrow/constants.py b/arrow/constants.py index f59b50183..2589592fd 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -34,3 +34,5 @@ MAX_ORDINAL: Final[int] = datetime.max.toordinal() MIN_ORDINAL: Final[int] = 1 + +DEFAULT_LOCALE: Final[str] = "en-us" diff --git a/arrow/factory.py b/arrow/factory.py index 1484796eb..27dea5c57 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -16,6 +16,7 @@ from arrow import parser from arrow.arrow import TZ_EXPR, Arrow +from arrow.constants import DEFAULT_LOCALE from arrow.util import is_timestamp, iso_to_gregorian @@ -36,7 +37,7 @@ def __init__(self, type: Type[Arrow] = Arrow) -> None: def get( self, *, - locale: str = "en_us", + locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, ) -> Arrow: @@ -57,7 +58,7 @@ def get( Tuple[int, int, int], ], *, - locale: str = "en_us", + locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, ) -> Arrow: @@ -69,7 +70,7 @@ def get( __arg1: Union[datetime, date], __arg2: TZ_EXPR, *, - locale: str = "en_us", + locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, ) -> Arrow: @@ -81,7 +82,7 @@ def get( __arg1: str, __arg2: Union[str, List[str]], *, - locale: str = "en_us", + locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, ) -> Arrow: @@ -90,7 +91,7 @@ def get( def get(self, *args: Any, **kwargs: Any) -> Arrow: """Returns an :class:`Arrow ` object based on flexible inputs. - :param locale: (optional) a ``str`` specifying a locale for the parser. Defaults to 'en_us'. + :param locale: (optional) a ``str`` specifying a locale for the parser. Defaults to 'en-us'. :param tzinfo: (optional) a :ref:`timezone expression ` or tzinfo object. Replaces the timezone unless using an input form that is explicitly UTC or specifies the timezone in a positional argument. Defaults to UTC. @@ -192,7 +193,7 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: """ arg_count = len(args) - locale = kwargs.pop("locale", "en_us") + locale = kwargs.pop("locale", DEFAULT_LOCALE) tz = kwargs.get("tzinfo", None) normalize_whitespace = kwargs.pop("normalize_whitespace", False) diff --git a/arrow/formatter.py b/arrow/formatter.py index 1744faec4..728bea1aa 100644 --- a/arrow/formatter.py +++ b/arrow/formatter.py @@ -8,6 +8,7 @@ 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 @@ -39,7 +40,7 @@ class DateTimeFormatter: locale: locales.Locale - def __init__(self, locale: str = "en_us") -> None: + def __init__(self, locale: str = DEFAULT_LOCALE) -> None: self.locale = locales.get_locale(locale) diff --git a/arrow/locales.py b/arrow/locales.py index 328050e64..14d762e3b 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -60,10 +60,11 @@ def get_locale(name: str) -> "Locale": """ - locale_cls = _locale_map.get(name.lower()) + normalized_locale_name = name.lower().replace("_", "-") + locale_cls = _locale_map.get(normalized_locale_name) if locale_cls is None: - raise ValueError(f"Unsupported locale {name!r}.") + raise ValueError(f"Unsupported locale {normalized_locale_name!r}.") return locale_cls() @@ -83,9 +84,6 @@ def get_locale_by_class_name(name: str) -> "Locale": return locale_cls() -# base locale type. - - class Locale: """ Represents locale-specific data and functionality. """ @@ -130,7 +128,7 @@ def __init_subclass__(cls, **kwargs: Any) -> None: if locale_name in _locale_map: raise LookupError(f"Duplicated locale name: {locale_name}") - _locale_map[locale_name] = cls + _locale_map[locale_name.lower().replace("_", "-")] = cls def __init__(self) -> None: @@ -290,21 +288,18 @@ def _format_relative( return direction.format(humanized) -# base locale type implementations. - - class EnglishLocale(Locale): names = [ "en", - "en_us", - "en_gb", - "en_au", - "en_be", - "en_jp", - "en_za", - "en_ca", - "en_ph", + "en-us", + "en-gb", + "en-au", + "en-be", + "en-jp", + "en-za", + "en-ca", + "en-ph", ] past = "{0} ago" @@ -408,7 +403,7 @@ def describe( class ItalianLocale(Locale): - names = ["it", "it_it"] + names = ["it", "it-it"] past = "{0} fa" future = "tra {0}" and_word = "e" @@ -481,7 +476,7 @@ def _ordinal_number(self, n: int) -> str: class SpanishLocale(Locale): - names = ["es", "es_es"] + names = ["es", "es-es"] past = "hace {0}" future = "en {0}" and_word = "y" @@ -619,7 +614,7 @@ def _ordinal_number(self, n: int) -> str: class FrenchLocale(FrenchBaseLocale, Locale): - names = ["fr", "fr_fr"] + names = ["fr", "fr-fr"] month_abbreviations = [ "", @@ -640,7 +635,7 @@ class FrenchLocale(FrenchBaseLocale, Locale): class FrenchCanadianLocale(FrenchBaseLocale, Locale): - names = ["fr_ca"] + names = ["fr-ca"] month_abbreviations = [ "", @@ -661,7 +656,7 @@ class FrenchCanadianLocale(FrenchBaseLocale, Locale): class GreekLocale(Locale): - names = ["el", "el_gr"] + names = ["el", "el-gr"] past = "{0} πριν" future = "σε {0}" @@ -729,7 +724,7 @@ class GreekLocale(Locale): class JapaneseLocale(Locale): - names = ["ja", "ja_jp"] + names = ["ja", "ja-jp"] past = "{0}前" future = "{0}後" @@ -790,7 +785,7 @@ class JapaneseLocale(Locale): class SwedishLocale(Locale): - names = ["sv", "sv_se"] + names = ["sv", "sv-se"] past = "för {0} sen" future = "om {0}" @@ -860,7 +855,7 @@ class SwedishLocale(Locale): class FinnishLocale(Locale): - names = ["fi", "fi_fi"] + names = ["fi", "fi-fi"] # The finnish grammar is very complex, and its hard to convert # 1-to-1 to something like English. @@ -957,7 +952,7 @@ def _ordinal_number(self, n: int) -> str: class ChineseCNLocale(Locale): - names = ["zh", "zh_cn"] + names = ["zh", "zh-cn"] past = "{0}前" future = "{0}后" @@ -1017,7 +1012,7 @@ class ChineseCNLocale(Locale): class ChineseTWLocale(Locale): - names = ["zh_tw"] + names = ["zh-tw"] past = "{0}前" future = "{0}後" @@ -1078,7 +1073,7 @@ class ChineseTWLocale(Locale): class HongKongLocale(Locale): - names = ["zh_hk"] + names = ["zh-hk"] past = "{0}前" future = "{0}後" @@ -1138,7 +1133,7 @@ class HongKongLocale(Locale): class KoreanLocale(Locale): - names = ["ko", "ko_kr"] + names = ["ko", "ko-kr"] past = "{0} 전" future = "{0} 후" @@ -1234,7 +1229,7 @@ def _format_relative( # derived locale types & implementations. class DutchLocale(Locale): - names = ["nl", "nl_nl"] + names = ["nl", "nl-nl"] past = "{0} geleden" future = "over {0}" @@ -1325,7 +1320,7 @@ def _format_timeframe( class BelarusianLocale(SlavicBaseLocale): - names = ["be", "be_by"] + names = ["be", "be-by"] past = "{0} таму" future = "праз {0}" @@ -1392,7 +1387,7 @@ class BelarusianLocale(SlavicBaseLocale): class PolishLocale(SlavicBaseLocale): - names = ["pl", "pl_pl"] + names = ["pl", "pl-pl"] past = "{0} temu" future = "za {0}" @@ -1463,7 +1458,7 @@ class PolishLocale(SlavicBaseLocale): class RussianLocale(SlavicBaseLocale): - names = ["ru", "ru_ru"] + names = ["ru", "ru-ru"] past = "{0} назад" future = "через {0}" @@ -1532,7 +1527,7 @@ class RussianLocale(SlavicBaseLocale): class AfrikaansLocale(Locale): - names = ["af", "af_nl"] + names = ["af", "af-nl"] past = "{0} gelede" future = "in {0}" @@ -1599,7 +1594,7 @@ class AfrikaansLocale(Locale): class BulgarianLocale(SlavicBaseLocale): - names = ["bg", "bg_BG"] + names = ["bg", "bg-bg"] past = "{0} назад" future = "напред {0}" @@ -1666,7 +1661,7 @@ class BulgarianLocale(SlavicBaseLocale): class UkrainianLocale(SlavicBaseLocale): - names = ["ua", "uk_ua"] + names = ["ua", "uk-ua"] past = "{0} тому" future = "за {0}" @@ -1732,7 +1727,7 @@ class UkrainianLocale(SlavicBaseLocale): class MacedonianLocale(SlavicBaseLocale): - names = ["mk", "mk_mk"] + names = ["mk", "mk-mk"] past = "пред {0}" future = "за {0}" @@ -1918,17 +1913,17 @@ def describe( class GermanLocale(GermanBaseLocale, Locale): - names = ["de", "de_de"] + names = ["de", "de-de"] class SwissLocale(GermanBaseLocale, Locale): - names = ["de_ch"] + names = ["de-ch"] class AustrianLocale(GermanBaseLocale, Locale): - names = ["de_at"] + names = ["de-at"] month_names = [ "", @@ -1949,7 +1944,7 @@ class AustrianLocale(GermanBaseLocale, Locale): class NorwegianLocale(Locale): - names = ["nb", "nb_no"] + names = ["nb", "nb-no"] past = "for {0} siden" future = "om {0}" @@ -2016,7 +2011,7 @@ class NorwegianLocale(Locale): class NewNorwegianLocale(Locale): - names = ["nn", "nn_no"] + names = ["nn", "nn-no"] past = "for {0} sidan" future = "om {0}" @@ -2082,7 +2077,7 @@ class NewNorwegianLocale(Locale): class PortugueseLocale(Locale): - names = ["pt", "pt_pt"] + names = ["pt", "pt-pt"] past = "há {0}" future = "em {0}" @@ -2151,14 +2146,14 @@ class PortugueseLocale(Locale): class BrazilianPortugueseLocale(PortugueseLocale): - names = ["pt_br"] + names = ["pt-br"] past = "faz {0}" class TagalogLocale(Locale): - names = ["tl", "tl_ph"] + names = ["tl", "tl-ph"] past = "nakaraang {0}" future = "{0} mula ngayon" @@ -2232,7 +2227,7 @@ def _ordinal_number(self, n: int) -> str: class VietnameseLocale(Locale): - names = ["vi", "vi_vn"] + names = ["vi", "vi-vn"] past = "{0} trước" future = "{0} nữa" @@ -2301,7 +2296,7 @@ class VietnameseLocale(Locale): class TurkishLocale(Locale): - names = ["tr", "tr_tr"] + names = ["tr", "tr-tr"] past = "{0} önce" future = "{0} sonra" @@ -2368,7 +2363,7 @@ class TurkishLocale(Locale): class AzerbaijaniLocale(Locale): - names = ["az", "az_az"] + names = ["az", "az-az"] past = "{0} əvvəl" future = "{0} sonra" @@ -2436,23 +2431,23 @@ class AzerbaijaniLocale(Locale): class ArabicLocale(Locale): names = [ "ar", - "ar_ae", - "ar_bh", - "ar_dj", - "ar_eg", - "ar_eh", - "ar_er", - "ar_km", - "ar_kw", - "ar_ly", - "ar_om", - "ar_qa", - "ar_sa", - "ar_sd", - "ar_so", - "ar_ss", - "ar_td", - "ar_ye", + "ar-ae", + "ar-bh", + "ar-dj", + "ar-eg", + "ar-eh", + "ar-er", + "ar-km", + "ar-kw", + "ar-ly", + "ar-om", + "ar-qa", + "ar-sa", + "ar-sd", + "ar-so", + "ar-ss", + "ar-td", + "ar-ye", ] past = "منذ {0}" @@ -2534,7 +2529,7 @@ def _format_timeframe( class LevantArabicLocale(ArabicLocale): - names = ["ar_iq", "ar_jo", "ar_lb", "ar_ps", "ar_sy"] + names = ["ar-iq", "ar-jo", "ar-lb", "ar-ps", "ar-sy"] month_names = [ "", "كانون الثاني", @@ -2568,7 +2563,7 @@ class LevantArabicLocale(ArabicLocale): class AlgeriaTunisiaArabicLocale(ArabicLocale): - names = ["ar_tn", "ar_dz"] + names = ["ar-tn", "ar-dz"] month_names = [ "", "جانفي", @@ -2602,7 +2597,7 @@ class AlgeriaTunisiaArabicLocale(ArabicLocale): class MauritaniaArabicLocale(ArabicLocale): - names = ["ar_mr"] + names = ["ar-mr"] month_names = [ "", "يناير", @@ -2636,7 +2631,7 @@ class MauritaniaArabicLocale(ArabicLocale): class MoroccoArabicLocale(ArabicLocale): - names = ["ar_ma"] + names = ["ar-ma"] month_names = [ "", "يناير", @@ -2682,7 +2677,7 @@ def _format_timeframe( return form.format(abs(delta)) # type: ignore - names = ["is", "is_is"] + names = ["is", "is-is"] past = "fyrir {0} síðan" future = "eftir {0}" @@ -2751,7 +2746,7 @@ def _format_timeframe( class DanishLocale(Locale): - names = ["da", "da_dk"] + names = ["da", "da-dk"] past = "for {0} siden" future = "efter {0}" @@ -2961,7 +2956,7 @@ class HindiLocale(Locale): class CzechLocale(Locale): - names = ["cs", "cs_cz"] + names = ["cs", "cs-cz"] timeframes: ClassVar[ Mapping[TimeFrameLiteral, Union[Mapping[str, Union[List[str], str]], str]] @@ -3060,7 +3055,7 @@ def _format_timeframe( class SlovakLocale(Locale): - names = ["sk", "sk_sk"] + names = ["sk", "sk-sk"] timeframes: ClassVar[ Mapping[TimeFrameLiteral, Union[Mapping[str, Union[List[str], str]], str]] @@ -3161,7 +3156,7 @@ def _format_timeframe( class FarsiLocale(Locale): - names = ["fa", "fa_ir"] + names = ["fa", "fa-ir"] past = "{0} قبل" future = "در {0}" @@ -3235,7 +3230,7 @@ class FarsiLocale(Locale): class HebrewLocale(Locale): - names = ["he", "he_IL"] + names = ["he", "he-il"] past = "לפני {0}" future = "בעוד {0}" @@ -3421,7 +3416,7 @@ class MarathiLocale(Locale): class CatalanLocale(Locale): - names = ["ca", "ca_es", "ca_ad", "ca_fr", "ca_it"] + names = ["ca", "ca-es", "ca-ad", "ca-fr", "ca-it"] past = "Fa {0}" future = "En {0}" and_word = "i" @@ -3495,7 +3490,7 @@ class CatalanLocale(Locale): class BasqueLocale(Locale): - names = ["eu", "eu_eu"] + names = ["eu", "eu-eu"] past = "duela {0}" future = "{0}" # I don't know what's the right phrase in Basque for the future. @@ -3560,7 +3555,7 @@ class BasqueLocale(Locale): class HungarianLocale(Locale): - names = ["hu", "hu_hu"] + names = ["hu", "hu-hu"] past = "{0} ezelőtt" future = "{0} múlva" @@ -3641,7 +3636,7 @@ def _format_timeframe( class EsperantoLocale(Locale): - names = ["eo", "eo_xx"] + names = ["eo", "eo-xx"] past = "antaŭ {0}" future = "post {0}" @@ -3714,7 +3709,7 @@ def _ordinal_number(self, n: int) -> str: class ThaiLocale(Locale): - names = ["th", "th_th"] + names = ["th", "th-th"] past = "{0}{1}ที่ผ่านมา" future = "ในอีก{1}{0}" @@ -3800,7 +3795,7 @@ def _format_relative( class BengaliLocale(Locale): - names = ["bn", "bn_bd", "bn_in"] + names = ["bn", "bn-bd", "bn-in"] past = "{0} আগে" future = "{0} পরে" @@ -3881,7 +3876,7 @@ def _ordinal_number(self, n: int) -> str: class RomanshLocale(Locale): - names = ["rm", "rm_ch"] + names = ["rm", "rm-ch"] past = "avant {0}" future = "en {0}" @@ -3949,7 +3944,7 @@ class RomanshLocale(Locale): class RomanianLocale(Locale): - names = ["ro", "ro_ro"] + names = ["ro", "ro-ro"] past = "{0} în urmă" future = "peste {0}" @@ -4016,7 +4011,7 @@ class RomanianLocale(Locale): class SlovenianLocale(Locale): - names = ["sl", "sl_si"] + names = ["sl", "sl-si"] past = "pred {0}" future = "čez {0}" @@ -4088,7 +4083,7 @@ class SlovenianLocale(Locale): class IndonesianLocale(Locale): - names = ["id", "id_id"] + names = ["id", "id-id"] past = "{0} yang lalu" future = "dalam {0}" @@ -4159,7 +4154,7 @@ class IndonesianLocale(Locale): class NepaliLocale(Locale): - names = ["ne", "ne_np"] + names = ["ne", "ne-np"] past = "{0} पहिले" future = "{0} पछी" @@ -4389,8 +4384,8 @@ class SwahiliLocale(Locale): names = [ "sw", - "sw_ke", - "sw_tz", + "sw-ke", + "sw-tz", ] past = "{0} iliyopita" diff --git a/arrow/parser.py b/arrow/parser.py index ca0eac18c..a1798a59a 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -25,6 +25,7 @@ from dateutil import tz from arrow import locales +from arrow.constants import DEFAULT_LOCALE from arrow.util import next_weekday, normalize_timestamp if sys.version_info < (3, 8): # pragma: no cover @@ -157,7 +158,7 @@ class DateTimeParser: locale: locales.Locale _input_re_map: Dict[_FORMAT_TYPE, Pattern[str]] - def __init__(self, locale: str = "en_us", cache_size: int = 0) -> None: + 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() diff --git a/tests/test_locales.py b/tests/test_locales.py index 7445a883d..5e7fa7ea6 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -9,7 +9,7 @@ class TestLocaleValidation: def test_locale_validation(self): - for _, locale_cls in self.locales.items(): + 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 assert locale_cls.day_names[0] == "" @@ -33,11 +33,22 @@ def test_locale_validation(self): assert locale_cls.past is not None 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 + assert locale_name.islower() + # Not a two-letter code + if len(locale_name) > 2: + assert "-" in locale_name + assert locale_name.count("-") == 1 + def test_duplicated_locale_name(self): with pytest.raises(LookupError): class Locale1(locales.Locale): - names = ["en_us"] + names = ["en-us"] class TestModule: @@ -47,13 +58,19 @@ def test_get_locale(self, mocker): mock_locale_cls.return_value = mock_locale with pytest.raises(ValueError): - arrow.locales.get_locale("locale_name") + arrow.locales.get_locale("locale-name") cls_dict = arrow.locales._locale_map - mocker.patch.dict(cls_dict, {"locale_name": mock_locale_cls}) + mocker.patch.dict(cls_dict, {"locale-name": mock_locale_cls}) result = arrow.locales.get_locale("locale_name") + assert result == mock_locale + + # Capitalization and hyphenation should still yield the same locale + result = arrow.locales.get_locale("locale-name") + assert result == mock_locale + result = arrow.locales.get_locale("locale-NAME") assert result == mock_locale def test_get_locale_by_class_name(self, mocker): @@ -77,6 +94,21 @@ def test_locales(self): assert len(locales._locale_map) > 0 +class TestCustomLocale: + def test_custom_locale_subclass(self): + class CustomLocale1(locales.Locale): + names = ["foo", "foo-BAR"] + + assert locales.get_locale("foo") is not None + assert locales.get_locale("foo-BAR") is not None + assert locales.get_locale("foo_bar") is not None + + class CustomLocale2(locales.Locale): + names = ["underscores_ok"] + + assert locales.get_locale("underscores_ok") is not None + + @pytest.mark.usefixtures("lang_locale") class TestEnglishLocale: def test_describe(self): diff --git a/tests/test_parser.py b/tests/test_parser.py index bdfb30614..1d5a541f4 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -861,7 +861,7 @@ def test_escape(self): assert escape_regex.findall("2018-03-09 8 [h] 40 [hello]") == ["[h]", "[hello]"] def test_month_names(self): - p = parser.DateTimeParser("en_us") + p = parser.DateTimeParser("en-us") text = "_".join(calendar.month_name[1:]) @@ -870,7 +870,7 @@ def test_month_names(self): assert result == calendar.month_name[1:] def test_month_abbreviations(self): - p = parser.DateTimeParser("en_us") + p = parser.DateTimeParser("en-us") text = "_".join(calendar.month_abbr[1:]) @@ -1435,12 +1435,12 @@ def test_month_alllower(self): ) def test_localized_month_name(self): - parser_ = parser.DateTimeParser("fr_fr") + parser_ = parser.DateTimeParser("fr-fr") assert parser_.parse("2013-Janvier-01", "YYYY-MMMM-DD") == datetime(2013, 1, 1) def test_localized_month_abbreviation(self): - parser_ = parser.DateTimeParser("it_it") + parser_ = parser.DateTimeParser("it-it") assert parser_.parse("2013-Gen-01", "YYYY-MMM-DD") == datetime(2013, 1, 1) @@ -1466,7 +1466,7 @@ def test_meridians_capitalized(self): ) def test_localized_meridians_lowercase(self): - parser_ = parser.DateTimeParser("hu_hu") + parser_ = parser.DateTimeParser("hu-hu") assert parser_.parse("2013-01-01 5 de", "YYYY-MM-DD h a") == datetime( 2013, 1, 1, 5 ) @@ -1476,7 +1476,7 @@ def test_localized_meridians_lowercase(self): ) def test_localized_meridians_capitalized(self): - parser_ = parser.DateTimeParser("hu_hu") + parser_ = parser.DateTimeParser("hu-hu") assert parser_.parse("2013-01-01 5 DE", "YYYY-MM-DD h A") == datetime( 2013, 1, 1, 5 ) @@ -1509,7 +1509,7 @@ def test_fr_meridians(self): @pytest.mark.usefixtures("dt_parser") class TestDateTimeParserMonthOrdinalDay: def test_english(self): - parser_ = parser.DateTimeParser("en_us") + parser_ = parser.DateTimeParser("en-us") assert parser_.parse("January 1st, 2013", "MMMM Do, YYYY") == datetime( 2013, 1, 1 @@ -1546,19 +1546,19 @@ def test_english(self): parser_.parse("January 11st, 2013", "MMMM Do, YYYY") def test_italian(self): - parser_ = parser.DateTimeParser("it_it") + parser_ = parser.DateTimeParser("it-it") assert parser_.parse("Gennaio 1º, 2013", "MMMM Do, YYYY") == datetime( 2013, 1, 1 ) def test_spanish(self): - parser_ = parser.DateTimeParser("es_es") + parser_ = parser.DateTimeParser("es-es") assert parser_.parse("Enero 1º, 2013", "MMMM Do, YYYY") == datetime(2013, 1, 1) def test_french(self): - parser_ = parser.DateTimeParser("fr_fr") + parser_ = parser.DateTimeParser("fr-fr") assert parser_.parse("Janvier 1er, 2013", "MMMM Do, YYYY") == datetime( 2013, 1, 1 @@ -1598,7 +1598,7 @@ def test_parse_search_with_names(self): ) def test_parse_search_locale_with_names(self): - p = parser.DateTimeParser("sv_se") + p = parser.DateTimeParser("sv-se") assert p.parse("Jan föddes den 31 Dec 1980", "DD MMM YYYY") == datetime( 1980, 12, 31 From 699198c4d2f7dee82f92803a87f6312710dcc387 Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Sun, 25 Apr 2021 12:34:58 -0400 Subject: [PATCH 515/649] Implement dehumanize method (#956) * Created new dehumanize method for Arrow objects. * Added comments as per style guide and improved input validation and test cases. Implemented fixes as per discussion. * Fixed issue with failed Bengali test case. * Correct grammar errors in comments and error messages fixed. * Added Dehumanize information to index.rst * Edited Dehumaize documentation in index.rst to be clearer. * Fixed linting issue with docs --- arrow/arrow.py | 131 ++++++++++- arrow/constants.py | 87 ++++++++ arrow/locales.py | 6 +- docs/index.rst | 35 +++ tests/test_arrow.py | 497 +++++++++++++++++++++++++++++++++++++++++- tests/test_locales.py | 2 +- 6 files changed, 752 insertions(+), 6 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 0de43889d..515c0ab66 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -6,6 +6,7 @@ import calendar +import re import sys from datetime import date from datetime import datetime as dt_datetime @@ -32,7 +33,7 @@ from dateutil.relativedelta import relativedelta from arrow import formatter, locales, parser, util -from arrow.constants import DEFAULT_LOCALE +from arrow.constants import DEFAULT_LOCALE, DEHUMANIZE_LOCALES from arrow.locales import TimeFrameLiteral if sys.version_info < (3, 8): # pragma: no cover @@ -1298,6 +1299,134 @@ def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float: "Please consider making a contribution to this locale." ) + def dehumanize(self, timestring: str, locale: str = "en_us") -> "Arrow": + """Returns a new :class:`Arrow ` object, that represents + the time difference relative to the attrbiutes of the + :class:`Arrow ` object. + + :param timestring: a ``str`` representing a humanized relative time. + :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en-us'. + + Usage:: + + >>> arw = arrow.utcnow() + >>> arw + + >>> earlier = arw.dehumanize("two days ago") + >>> earlier + + + >>> arw = arrow.utcnow() + >>> arw + + >>> later = arw.dehumanize("in 1 month") + >>> later + + + """ + + # Create a locale object based off given local + locale_obj = locales.get_locale(locale) + + # Check to see if locale is supported + normalized_locale_name = locale.lower().replace("_", "-") + + if normalized_locale_name not in DEHUMANIZE_LOCALES: + raise ValueError( + f"Dehumanize does not currently support the {locale} locale, please consider making a contribution to add support for this locale." + ) + + current_time = self.fromdatetime(self._datetime) + + # Create an object containing the relative time info + time_object_info = dict.fromkeys( + ["seconds", "minutes", "hours", "days", "weeks", "months", "years"], 0 + ) + + # Create an object representing if unit has been seen + unit_visited = dict.fromkeys( + ["now", "seconds", "minutes", "hours", "days", "weeks", "months", "years"], + False, + ) + + # Create a regex pattern object for numbers + num_pattern = re.compile(r"\d+") + + # Search timestring for each time unit within locale + for unit in locale_obj.timeframes: + + # Numeric unit of change + change_value = 0 + + # Replace {0} with regex \d representing digits + search_string = str(locale_obj.timeframes[unit]) + search_string = search_string.format(r"\d+") + + # Create search pattern and find within string + pattern = re.compile(fr"{search_string}") + match = pattern.search(timestring) + + # If there is no match continue to next iteration + if not match: + continue + + match_string = match.group() + num_match = num_pattern.search(match_string) + + # If no number matches set change value to be one + if not num_match: + change_value = 1 + else: + change_value = int(num_match.group()) + + # No time to update if now is the unit + if unit == "now": + unit_visited[unit] = True + continue + + # Add change value to the correct unit (incorporates the plurality that exists within timeframe i.e second v.s seconds) + time_unit_to_change = str(unit) + time_unit_to_change += "s" if (str(time_unit_to_change)[-1] != "s") else "" + time_object_info[time_unit_to_change] = change_value + unit_visited[time_unit_to_change] = True + + # 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. + If you are attempting to use the week granularity on an unsupported locale, this could be the cause of this error.""" + ) + + # Sign logic + future_string = locale_obj.future + future_string = future_string.format(".*") + future_pattern = re.compile(fr"^{future_string}$") + future_pattern_match = future_pattern.findall(timestring) + + past_string = locale_obj.past + past_string = past_string.format(".*") + past_pattern = re.compile(fr"^{past_string}$") + past_pattern_match = past_pattern.findall(timestring) + + # If a string contains the now unit, there will be no relative units, hence the need to check if the now unit + # was visited before raising a ValueError + if past_pattern_match: + sign_val = -1 + elif future_pattern_match: + sign_val = 1 + elif unit_visited["now"]: + sign_val = 0 + else: + raise ValueError( + """Invalid input String. String does not contain any relative time information. + String should either represent a time in the future or a time in the past. + Ex: "in 5 seconds" or "5 seconds ago". """ + ) + + time_changes = {k: sign_val * v for k, v in time_object_info.items()} + + return current_time.shift(**time_changes) + # query functions def is_between( diff --git a/arrow/constants.py b/arrow/constants.py index 2589592fd..23d886a4e 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -36,3 +36,90 @@ MIN_ORDINAL: Final[int] = 1 DEFAULT_LOCALE: Final[str] = "en-us" + +# Supported dehumanize locales +DEHUMANIZE_LOCALES = { + "en", + "en-us", + "en-gb", + "en-au", + "en-be", + "en-jp", + "en-za", + "en-ca", + "en-ph", + "fr", + "fr-fr", + "fr-ca", + "it", + "it-it", + "es", + "es-es", + "el", + "el-gr", + "ja", + "ja-jp", + "sv", + "sv-se", + "zh", + "zh-cn", + "zh-tw", + "zh-hk", + "nl", + "nl-nl", + "af", + "de", + "de-de", + "de-ch", + "de-at", + "nb", + "nb-no", + "nn", + "nn-no", + "pt", + "pt-pt", + "pt-br", + "tl", + "tl-ph", + "vi", + "vi-vn", + "tr", + "tr-tr", + "az", + "az-az", + "da", + "da-dk", + "ml", + "hi", + "fa", + "fa-ir", + "mr", + "ca", + "ca-es", + "ca-ad", + "ca-fr", + "ca-it", + "eo", + "eo-xx", + "bn", + "bn-bd", + "bn-in", + "rm", + "rm-ch", + "ro", + "ro-ro", + "sl", + "sl-si", + "id", + "id-id", + "sw", + "sw-ke", + "sw-tz", + "la", + "la-va", + "lt", + "lt-lt", + "ms", + "ms-my", + "ms-bn", +} diff --git a/arrow/locales.py b/arrow/locales.py index 14d762e3b..c604dfafb 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2929,7 +2929,7 @@ class HindiLocale(Locale): month_abbreviations = [ "", "जन", - "फ़र", + "फ़र", "मार्च", "अप्रै", "मई", @@ -3821,7 +3821,7 @@ class BengaliLocale(Locale): month_names = [ "", "জানুয়ারি", - "ফেব্রুয়ারি", + "ফেব্রুয়ারি", "মার্চ", "এপ্রিল", "মে", @@ -3867,7 +3867,7 @@ def _ordinal_number(self, n: int) -> str: if n in [1, 5, 7, 8, 9, 10]: return f"{n}ম" if n in [2, 3]: - return f"{n}য়" + return f"{n}য়" if n == 4: return f"{n}র্থ" if n == 6: diff --git a/docs/index.rst b/docs/index.rst index 42bd73fda..3cef5a9b9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -252,6 +252,41 @@ Support for a growing number of locales (see ``locales.py`` for supported langua >>> 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("two days ago") + >>> earlier + + +Or use it to shift into a future time: + +.. code-block:: python + + >>> arw = arrow.utcnow() + >>> arw + + >>> later = arw.dehumanize("in 1 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 ~~~~~~~~~~~~~~ diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 473af36b5..78211f8a8 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -10,7 +10,7 @@ from dateutil import tz from dateutil.relativedelta import FR, MO, SA, SU, TH, TU, WE -from arrow import arrow +from arrow import arrow, locales from .utils import assert_datetime_equality @@ -2293,6 +2293,501 @@ def test_years(self): assert result == "год назад" +# Fixtures for Dehumanize +@pytest.fixture(scope="class") +def locale_list_no_weeks(): + tested_langs = [ + "en", + "en-us", + "en-gb", + "en-au", + "en-be", + "en-jp", + "en-za", + "en-ca", + "en-ph", + "fr", + "fr-fr", + "fr-ca", + "it", + "it-it", + "es", + "es-es", + "el", + "el-gr", + "ja", + "ja-jp", + "sv", + "sv-se", + "zh", + "zh-cn", + "zh-tw", + "zh-hk", + "nl", + "nl-nl", + "af", + "de", + "de-de", + "de-ch", + "de-at", + "nb", + "nb-no", + "nn", + "nn-no", + "pt", + "pt-pt", + "pt_br", + "tl", + "tl-ph", + "vi", + "vi-vn", + "tr", + "tr-tr", + "az", + "az-az", + "da", + "da-dk", + "ml", + "hi", + "fa", + "fa-ir", + "mr", + "ca", + "ca-es", + "ca-ad", + "ca-fr", + "ca-it", + "eo", + "eo-xx", + "bn", + "bn-bd", + "bn-in", + "rm", + "rm-ch", + "ro", + "ro-ro", + "sl", + "sl-si", + "id", + "id-id", + "sw", + "sw-ke", + "sw-tz", + "la", + "la-va", + "lt", + "lt-lt", + "ms", + "ms-my", + "ms-bn", + ] + + return tested_langs + + +@pytest.fixture(scope="class") +def locale_list_with_weeks(): + tested_langs = [ + "en", + "en-us", + "en-gb", + "en-au", + "en-be", + "en-jp", + "en-za", + "en-ca", + "en-ph", + "fr", + "fr-fr", + "fr-ca", + "it", + "it-it", + "es", + "es-es", + "ja", + "ja-jp", + "sv", + "sv-se", + "zh", + "zh-cn", + "zh-tw", + "zh-hk", + "nl", + "nl-nl", + "de", + "de-de", + "de-ch", + "de-at", + "pt", + "pt-pt", + "pt-br", + "tl", + "tl-ph", + "vi", + "vi-vn", + "sw", + "sw-ke", + "sw-tz", + "la", + "la-va", + "lt", + "lt-lt", + "ms", + "ms-my", + "ms-bn", + ] + + return tested_langs + + +class TestArrowDehumanize: + def test_now(self, locale_list_no_weeks): + + 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) + + second_ago_string = second_ago.humanize( + arw, locale=lang, granularity=["second"] + ) + second_future_string = second_future.humanize( + arw, locale=lang, granularity=["second"] + ) + + assert arw.dehumanize(second_ago_string, locale=lang) == arw + assert arw.dehumanize(second_future_string, locale=lang) == arw + + def test_seconds(self, locale_list_no_weeks): + + 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) + + second_ago_string = second_ago.humanize( + arw, locale=lang, granularity=["second"] + ) + second_future_string = second_future.humanize( + arw, locale=lang, granularity=["second"] + ) + + assert arw.dehumanize(second_ago_string, locale=lang) == second_ago + assert arw.dehumanize(second_future_string, locale=lang) == second_future + + def test_minute(self, locale_list_no_weeks): + + 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) + + minute_ago_string = minute_ago.humanize( + arw, locale=lang, granularity=["minute"] + ) + minute_future_string = minute_future.humanize( + arw, locale=lang, granularity=["minute"] + ) + + assert arw.dehumanize(minute_ago_string, locale=lang) == minute_ago + assert arw.dehumanize(minute_future_string, locale=lang) == minute_future + + def test_minutes(self, locale_list_no_weeks): + + 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) + + minute_ago_string = minute_ago.humanize( + arw, locale=lang, granularity=["minute"] + ) + minute_future_string = minute_future.humanize( + arw, locale=lang, granularity=["minute"] + ) + + assert arw.dehumanize(minute_ago_string, locale=lang) == minute_ago + assert arw.dehumanize(minute_future_string, locale=lang) == minute_future + + def test_hour(self, locale_list_no_weeks): + + 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) + + hour_ago_string = hour_ago.humanize(arw, locale=lang, granularity=["hour"]) + hour_future_string = hour_future.humanize( + arw, locale=lang, granularity=["hour"] + ) + + assert arw.dehumanize(hour_ago_string, locale=lang) == hour_ago + assert arw.dehumanize(hour_future_string, locale=lang) == hour_future + + def test_hours(self, locale_list_no_weeks): + + 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) + + hour_ago_string = hour_ago.humanize(arw, locale=lang, granularity=["hour"]) + hour_future_string = hour_future.humanize( + arw, locale=lang, granularity=["hour"] + ) + + assert arw.dehumanize(hour_ago_string, locale=lang) == hour_ago + assert arw.dehumanize(hour_future_string, locale=lang) == hour_future + + def test_week(self, locale_list_with_weeks): + + 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) + + week_ago_string = week_ago.humanize(arw, locale=lang, granularity=["week"]) + week_future_string = week_future.humanize( + arw, locale=lang, granularity=["week"] + ) + + assert arw.dehumanize(week_ago_string, locale=lang) == week_ago + assert arw.dehumanize(week_future_string, locale=lang) == week_future + + def test_weeks(self, locale_list_with_weeks): + + 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) + + week_ago_string = week_ago.humanize(arw, locale=lang, granularity=["week"]) + week_future_string = week_future.humanize( + arw, locale=lang, granularity=["week"] + ) + + assert arw.dehumanize(week_ago_string, locale=lang) == week_ago + assert arw.dehumanize(week_future_string, locale=lang) == week_future + + def test_year(self, locale_list_no_weeks): + + 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) + + year_ago_string = year_ago.humanize(arw, locale=lang, granularity=["year"]) + year_future_string = year_future.humanize( + arw, locale=lang, granularity=["year"] + ) + + assert arw.dehumanize(year_ago_string, locale=lang) == year_ago + assert arw.dehumanize(year_future_string, locale=lang) == year_future + + def test_years(self, locale_list_no_weeks): + + 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) + + year_ago_string = year_ago.humanize(arw, locale=lang, granularity=["year"]) + year_future_string = year_future.humanize( + arw, locale=lang, granularity=["year"] + ) + + assert arw.dehumanize(year_ago_string, locale=lang) == year_ago + assert arw.dehumanize(year_future_string, locale=lang) == year_future + + def test_mixed_granularity(self, locale_list_no_weeks): + + 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) + + past_string = past.humanize( + arw, locale=lang, granularity=["hour", "minute", "second"] + ) + future_string = future.humanize( + arw, locale=lang, granularity=["hour", "minute", "second"] + ) + + assert arw.dehumanize(past_string, locale=lang) == past + assert arw.dehumanize(future_string, locale=lang) == future + + def test_mixed_granularity_hours(self, locale_list_no_weeks): + + 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) + + past_string = past.humanize( + arw, locale=lang, granularity=["hour", "minute", "second"] + ) + future_string = future.humanize( + arw, locale=lang, granularity=["hour", "minute", "second"] + ) + + assert arw.dehumanize(past_string, locale=lang) == past + assert arw.dehumanize(future_string, locale=lang) == future + + def test_mixed_granularity_day(self, locale_list_no_weeks): + + 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) + + past_string = past.humanize( + arw, locale=lang, granularity=["day", "minute", "second"] + ) + future_string = future.humanize( + arw, locale=lang, granularity=["day", "minute", "second"] + ) + + assert arw.dehumanize(past_string, locale=lang) == past + assert arw.dehumanize(future_string, locale=lang) == future + + def test_mixed_granularity_day_hour(self, locale_list_no_weeks): + + 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) + + past_string = past.humanize( + arw, locale=lang, granularity=["day", "hour", "second"] + ) + future_string = future.humanize( + arw, locale=lang, granularity=["day", "hour", "second"] + ) + + assert arw.dehumanize(past_string, locale=lang) == past + assert arw.dehumanize(future_string, locale=lang) == future + + # 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) + + second_ago_string = second_ago.humanize( + arw, locale="fi", granularity=["second"] + ) + second_future_string = second_future.humanize( + arw, locale="fi", granularity=["second"] + ) + + # fi is an example of many unsupported locales currently + with pytest.raises(ValueError): + arw.dehumanize(second_ago_string, locale="fi") + + with pytest.raises(ValueError): + arw.dehumanize(second_future_string, locale="fi") + + # 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) + + second_ago_string = second_ago.humanize( + arw, locale="zh_hk", granularity=["second"] + ) + second_future_string = second_future.humanize( + arw, locale="zh_hk", granularity=["second"] + ) + + assert arw.dehumanize(second_ago_string, locale="zh_hk") == second_ago + assert arw.dehumanize(second_future_string, locale="zh_hk") == second_future + + # Ensures relative units are required in string + def test_require_relative_unit(self, locale_list_no_weeks): + + 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) + + second_ago_string = second_ago.humanize( + arw, locale=lang, granularity=["second"], only_distance=True + ) + second_future_string = second_future.humanize( + arw, locale=lang, granularity=["second"], only_distance=True + ) + + with pytest.raises(ValueError): + arw.dehumanize(second_ago_string, locale=lang) + + with pytest.raises(ValueError): + arw.dehumanize(second_future_string, locale=lang) + + # Test for scrambled input + def test_scrambled_input(self, locale_list_no_weeks): + + 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) + + second_ago_string = second_ago.humanize( + arw, locale=lang, granularity=["second"], only_distance=True + ) + second_future_string = second_future.humanize( + arw, locale=lang, granularity=["second"], only_distance=True + ) + + # Scrambles input by sorting strings + second_ago_presort = sorted(second_ago_string) + second_ago_string = "".join(second_ago_presort) + + second_future_presort = sorted(second_future_string) + second_future_string = "".join(second_future_presort) + + with pytest.raises(ValueError): + arw.dehumanize(second_ago_string, locale=lang) + + with pytest.raises(ValueError): + arw.dehumanize(second_future_string, locale=lang) + + def test_no_units_modified(self, locale_list_no_weeks): + + 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 + locale_obj = locales.get_locale(lang) + empty_past_string = locale_obj.past + empty_future_string = locale_obj.future + + with pytest.raises(ValueError): + arw.dehumanize(empty_past_string, locale=lang) + + with pytest.raises(ValueError): + arw.dehumanize(empty_future_string, locale=lang) + + class TestArrowIsBetween: def test_start_before_end(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7)) diff --git a/tests/test_locales.py b/tests/test_locales.py index 5e7fa7ea6..85d1cadbd 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -834,7 +834,7 @@ class TestBengaliLocale: def test_ordinal_number(self): assert self.locale._ordinal_number(0) == "0তম" assert self.locale._ordinal_number(1) == "1ম" - assert self.locale._ordinal_number(3) == "3য়" + assert self.locale._ordinal_number(3) == "3য়" assert self.locale._ordinal_number(4) == "4র্থ" assert self.locale._ordinal_number(5) == "5ম" assert self.locale._ordinal_number(6) == "6ষ্ঠ" From fbec661ad26555bfde877b1e5121d86ae69215da Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Tue, 27 Apr 2021 23:05:28 +0100 Subject: [PATCH 516/649] Bump version to 1.1.0 and update CHANGELOG (#963) * Bump version to 1.1.0 and update CHANGELOG * Suggested changes * Break up comment --- CHANGELOG.rst | 48 +++++++++++++++++++++++++++++++++++++++++++++++ arrow/_version.py | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 300af2089..fe1cf1fee 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,54 @@ Changelog ========= +1.1.0 (2021-04-26) +------------------ + +- [NEW] Implemented the ``dehumanize`` method for ``Arrow`` objects. This takes human readable input and uses it to perform relative time shifts, for example: + +.. code-block:: python + + >>> arw + + >>> arw.dehumanize("8 hours ago") + + >>> arw.dehumanize("in 4 days") + + >>> arw.dehumanize("in an hour 34 minutes 10 seconds") + + >>> arw.dehumanize("hace 2 años", locale="es") + + +- [NEW] Made the start of the week adjustable when using ``span("week")``, for example: + +.. code-block:: python + + >>> arw + + >>> arw.isoweekday() + 1 # Monday + >>> arw.span("week") + (, ) + >>> arw.span("week", week_start=4) + (, ) + +- [NEW] Added Croatian, Latin, Latvian, Lithuanian and Malay locales. +- [FIX] Internally standardize locales and improve locale validation. Locales should now use the ISO notation of a dash (``"en-gb"``) rather than an underscore (``"en_gb"``) however this change is backward compatible. +- [FIX] Correct type checking for internal locale mapping by using ``_init_subclass``. This now allows subclassing of locales, for example: + +.. code-block:: python + + >>> from arrow.locales import EnglishLocale + >>> class Klingon(EnglishLocale): + ... names = ["tlh"] + ... + >>> from arrow import locales + >>> locales.get_locale("tlh") + <__main__.Klingon object at 0x7f7cd1effd30> + +- [FIX] Correct type checking for ``arrow.get(2021, 3, 9)`` construction. +- [FIX] Audited all docstrings for style, typos and outdated info. + 1.0.3 (2021-03-05) ------------------ diff --git a/arrow/_version.py b/arrow/_version.py index 976498ab9..6849410aa 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "1.0.3" +__version__ = "1.1.0" From 08705bcde7a8eef65a40f43acd733863d8c1f63b Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Tue, 27 Apr 2021 20:46:27 -0400 Subject: [PATCH 517/649] Reverse humanize (#965) * changes * Added test cases * Added more langauges * Added more langauge test cases and simplified code * fixed merge issue with locales.py * Added additional locales to test case files * changes * Added test cases * Added more langauges * Added more langauge test cases and simplified code * fixed merge issue with locales.py * Added additional locales to test case files * Added comments as per style guide. Added more input validation and input validation test cases. Implemented fixes as per discussion. * Fixed issue with failed Bengali test case * Grammar errors in comments and error messages fixed. * Added Dehumanize information to index.rst * Edited Dehumaize documentation in index.rst to be more clear * Fixed linting issue with docs * Updated error in the documentation, as well as changed value error strings to format better * Fixed typo in dehumanize doc * White space error fixed --- arrow/arrow.py | 14 +++++++------- docs/index.rst | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 515c0ab66..b962c6561 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1312,14 +1312,14 @@ def dehumanize(self, timestring: str, locale: str = "en_us") -> "Arrow": >>> arw = arrow.utcnow() >>> arw - >>> earlier = arw.dehumanize("two days ago") + >>> earlier = arw.dehumanize("2 days ago") >>> earlier >>> arw = arrow.utcnow() >>> arw - >>> later = arw.dehumanize("in 1 month") + >>> later = arw.dehumanize("in a month") >>> later @@ -1393,8 +1393,8 @@ def dehumanize(self, timestring: 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. - If you are attempting to use the week granularity on an unsupported locale, this could be the cause of this error.""" + "Input string not valid. Note: Some locales do not support the week granulairty in Arrow. " + "If you are attempting to use the week granularity on an unsupported locale, this could be the cause of this error." ) # Sign logic @@ -1418,9 +1418,9 @@ def dehumanize(self, timestring: str, locale: str = "en_us") -> "Arrow": sign_val = 0 else: raise ValueError( - """Invalid input String. String does not contain any relative time information. - String should either represent a time in the future or a time in the past. - Ex: "in 5 seconds" or "5 seconds ago". """ + "Invalid input String. String does not contain any relative time information. " + "String should either represent a time in the future or a time in the past. " + "Ex: 'in 5 seconds' or '5 seconds ago'." ) time_changes = {k: sign_val * v for k, v in time_object_info.items()} diff --git a/docs/index.rst b/docs/index.rst index 3cef5a9b9..43895b044 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -262,7 +262,7 @@ Take a human readable string and use it to shift into a past time: >>> arw = arrow.utcnow() >>> arw - >>> earlier = arw.dehumanize("two days ago") + >>> earlier = arw.dehumanize("2 days ago") >>> earlier @@ -273,7 +273,7 @@ Or use it to shift into a future time: >>> arw = arrow.utcnow() >>> arw - >>> later = arw.dehumanize("in 1 month") + >>> later = arw.dehumanize("in a month") >>> later From fe1aaaee3e14ccb256dd2aa74e07c84226cd4b05 Mon Sep 17 00:00:00 2001 From: snmishra Date: Thu, 29 Apr 2021 14:32:12 -0500 Subject: [PATCH 518/649] Add Odia (or) locale (#966) * Add Odia (or) locale * Fix formatting with black * Improve Odia translation * Add a few more tests --- arrow/constants.py | 2 + arrow/locales.py | 91 +++++++++++++++++++++++++++++++++++++++++++ tests/test_arrow.py | 2 + tests/test_locales.py | 35 +++++++++++++++++ 4 files changed, 130 insertions(+) diff --git a/arrow/constants.py b/arrow/constants.py index 23d886a4e..5986b6104 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -122,4 +122,6 @@ "ms", "ms-my", "ms-bn", + "or", + "or-in", } diff --git a/arrow/locales.py b/arrow/locales.py index c604dfafb..970d61453 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -4800,3 +4800,94 @@ class MalayLocale(Locale): "Sabtu", "Ahad ", ] + + +class OdiaLocale(Locale): + + names = ["or", "or-in"] + + past = "{0} ପୂର୍ବେ" + future = "{0} ପରେ" + + timeframes = { + "now": "ବର୍ତ୍ତମାନ", + "second": "ଏକ ସେକେଣ୍ଡ", + "seconds": "{0} ସେକେଣ୍ଡ", + "minute": "ଏକ ମିନଟ", + "minutes": "{0} ମିନଟ", + "hour": "ଏକ ଘଣ୍ଟା", + "hours": "{0} ଘଣ୍ଟା", + "day": "ଏକ ଦିନ", + "days": "{0} ଦିନ", + "month": "ଏକ ମାସ", + "months": "{0} ମାସ ", + "year": "ଏକ ବର୍ଷ", + "years": "{0} ବର୍ଷ", + } + + meridians = {"am": "ପୂର୍ବାହ୍ନ", "pm": "ଅପରାହ୍ନ", "AM": "ପୂର୍ବାହ୍ନ", "PM": "ଅପରାହ୍ନ"} + + month_names = [ + "", + "ଜାନୁଆରୀ", + "ଫେବୃଆରୀ", + "ମାର୍ଚ୍ଚ୍", + "ଅପ୍ରେଲ", + "ମଇ", + "ଜୁନ୍", + "ଜୁଲାଇ", + "ଅଗଷ୍ଟ", + "ସେପ୍ଟେମ୍ବର", + "ଅକ୍ଟୋବର୍", + "ନଭେମ୍ବର୍", + "ଡିସେମ୍ବର୍", + ] + month_abbreviations = [ + "", + "ଜାନୁ", + "ଫେବୃ", + "ମାର୍ଚ୍ଚ୍", + "ଅପ୍ରେ", + "ମଇ", + "ଜୁନ୍", + "ଜୁଲା", + "ଅଗ", + "ସେପ୍ଟେ", + "ଅକ୍ଟୋ", + "ନଭେ", + "ଡିସେ", + ] + + day_names = [ + "", + "ସୋମବାର", + "ମଙ୍ଗଳବାର", + "ବୁଧବାର", + "ଗୁରୁବାର", + "ଶୁକ୍ରବାର", + "ଶନିବାର", + "ରବିବାର", + ] + day_abbreviations = [ + "", + "ସୋମ", + "ମଙ୍ଗଳ", + "ବୁଧ", + "ଗୁରୁ", + "ଶୁକ୍ର", + "ଶନି", + "ରବି", + ] + + def _ordinal_number(self, n: int) -> str: + if n > 10 or n == 0: + return f"{n}ତମ" + if n in [1, 5, 7, 8, 9, 10]: + return f"{n}ମ" + if n in [2, 3]: + return f"{n}ୟ" + if n == 4: + return f"{n}ର୍ଥ" + if n == 6: + return f"{n}ଷ୍ଠ" + return "" diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 78211f8a8..bc72bf317 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2380,6 +2380,8 @@ def locale_list_no_weeks(): "ms", "ms-my", "ms-bn", + "or", + "or-in", ] return tested_langs diff --git a/tests/test_locales.py b/tests/test_locales.py index 85d1cadbd..7a9c3028a 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1549,3 +1549,38 @@ def test_plurals(self): assert self.locale._format_timeframe("months", 11) == "11 månader" assert self.locale._format_timeframe("year", 1) == "ett år" assert self.locale._format_timeframe("years", 12) == "12 år" + + +@pytest.mark.usefixtures("lang_locale") +class TestOdiaLocale: + def test_ordinal_number(self): + assert self.locale._ordinal_number(0) == "0ତମ" + assert self.locale._ordinal_number(1) == "1ମ" + assert self.locale._ordinal_number(3) == "3ୟ" + assert self.locale._ordinal_number(4) == "4ର୍ଥ" + assert self.locale._ordinal_number(5) == "5ମ" + assert self.locale._ordinal_number(6) == "6ଷ୍ଠ" + 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) == "" + + 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 == "ଏକ ଘଣ୍ଟା ପୂର୍ବେ" From 06fe693c193add28c541781e28edee132cb33d77 Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Mon, 3 May 2021 03:47:56 +0100 Subject: [PATCH 519/649] Fix multiple bugs in arrow.get() tzinfo kwarg handling (#968) * Fix bug in factory.get(datetime, tzinfo=...) * Fix bug in factory.get(Arrow, tzinfo=...) * Change behaviour of factory.get() with date objects * Fix bug in factory.get(iso_calendar, tzinfo=...) * Make tzinfo kwarg clear in get() factory conditions Co-authored-by: Jad Chaar Co-authored-by: Jad Chaar --- arrow/factory.py | 42 +++++++++++++++++++++--------------------- tests/test_factory.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/arrow/factory.py b/arrow/factory.py index 27dea5c57..5787dcf02 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -205,14 +205,14 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: if len(kwargs) == 1 and tz is None: arg_count = 3 - # () -> now, @ utc. + # () -> now, @ tzinfo or utc if arg_count == 0: if isinstance(tz, str): tz = parser.TzinfoParser.parse(tz) - return self.type.now(tz) + return self.type.now(tzinfo=tz) if isinstance(tz, dt_tzinfo): - return self.type.now(tz) + return self.type.now(tzinfo=tz) return self.type.utcnow() @@ -223,42 +223,42 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: if arg is None: raise TypeError("Cannot parse argument of type None.") - # try (int, float) -> from timestamp with tz + # try (int, float) -> from timestamp @ tzinfo elif not isinstance(arg, str) and is_timestamp(arg): if tz is None: # set to UTC by default tz = dateutil_tz.tzutc() return self.type.fromtimestamp(arg, tzinfo=tz) - # (Arrow) -> from the object's datetime. + # (Arrow) -> from the object's datetime @ tzinfo elif isinstance(arg, Arrow): - return self.type.fromdatetime(arg.datetime) + return self.type.fromdatetime(arg.datetime, tzinfo=tz) - # (datetime) -> from datetime. + # (datetime) -> from datetime @ tzinfo elif isinstance(arg, datetime): - return self.type.fromdatetime(arg) + return self.type.fromdatetime(arg, tzinfo=tz) - # (date) -> from date. + # (date) -> from date @ tzinfo elif isinstance(arg, date): - return self.type.fromdate(arg) + return self.type.fromdate(arg, tzinfo=tz) - # (tzinfo) -> now, @ tzinfo. + # (tzinfo) -> now @ tzinfo elif isinstance(arg, dt_tzinfo): - return self.type.now(arg) + return self.type.now(tzinfo=arg) - # (str) -> parse. + # (str) -> parse @ tzinfo elif isinstance(arg, str): dt = parser.DateTimeParser(locale).parse_iso(arg, normalize_whitespace) - return self.type.fromdatetime(dt, tz) + return self.type.fromdatetime(dt, tzinfo=tz) # (struct_time) -> from struct_time elif isinstance(arg, struct_time): return self.type.utcfromtimestamp(calendar.timegm(arg)) - # (iso calendar) -> convert then from date + # (iso calendar) -> convert then from date @ tzinfo elif isinstance(arg, tuple) and len(arg) == 3: d = iso_to_gregorian(*arg) - return self.type.fromdate(d) + return self.type.fromdate(d, tzinfo=tz) else: raise TypeError(f"Cannot parse single argument of type {type(arg)!r}.") @@ -269,9 +269,9 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: if isinstance(arg_1, datetime): - # (datetime, tzinfo/str) -> fromdatetime replace tzinfo. + # (datetime, tzinfo/str) -> fromdatetime @ tzinfo if isinstance(arg_2, (dt_tzinfo, str)): - return self.type.fromdatetime(arg_1, arg_2) + return self.type.fromdatetime(arg_1, tzinfo=arg_2) else: raise TypeError( f"Cannot parse two arguments of types 'datetime', {type(arg_2)!r}." @@ -279,7 +279,7 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: elif isinstance(arg_1, date): - # (date, tzinfo/str) -> fromdate replace tzinfo. + # (date, tzinfo/str) -> fromdate @ tzinfo if isinstance(arg_2, (dt_tzinfo, str)): return self.type.fromdate(arg_1, tzinfo=arg_2) else: @@ -287,7 +287,7 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: f"Cannot parse two arguments of types 'date', {type(arg_2)!r}." ) - # (str, format) -> parse. + # (str, format) -> parse @ tzinfo elif isinstance(arg_1, str) and isinstance(arg_2, (str, list)): dt = parser.DateTimeParser(locale).parse( args[0], args[1], normalize_whitespace @@ -299,7 +299,7 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: f"Cannot parse two arguments of types {type(arg_1)!r} and {type(arg_2)!r}." ) - # 3+ args -> datetime-like via constructor. + # 3+ args -> datetime-like via constructor else: return self.type(*args, **kwargs) diff --git a/tests/test_factory.py b/tests/test_factory.py index 5e0020d65..4bb81e872 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -4,6 +4,7 @@ import pytest from dateutil import tz +from arrow import Arrow from arrow.parser import ParserError from .utils import assert_datetime_equality @@ -172,6 +173,46 @@ def test_kwarg_normalize_whitespace(self): 2013, 5, 5, 12, 30, 45, 123456, tzinfo=tz.tzutc() ) + # 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") + + expected = datetime(2021, 4, 29, 6, tzinfo=tz.gettz("America/Chicago")) + + 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") + + expected = datetime(2021, 4, 29, 6, tzinfo=tz.gettz("America/Chicago")) + + 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") + + expected = Arrow(2021, 4, 29, tzinfo=tz.gettz("America/Chicago")) + + assert result.date() == expected.date() + 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") + + assert_datetime_equality(result, expected) + def test_one_arg_iso_str(self): dt = datetime.utcnow() From 384fb740816824c2eec44dd700d7b6fa465137a5 Mon Sep 17 00:00:00 2001 From: cyriaka90 Date: Thu, 6 May 2021 07:14:17 +0200 Subject: [PATCH 520/649] Add Maltese locale (#972) --- arrow/locales.py | 98 ++++++++++++++++++++++++++++++++++++++++++- tests/test_locales.py | 27 ++++++++++++ 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 970d61453..6a3409bd8 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -4787,7 +4787,7 @@ class MalayLocale(Locale): "Khamis", "Jumaat", "Sabtu", - "Ahad ", + "Ahad", ] day_abbreviations = [ @@ -4798,10 +4798,104 @@ class MalayLocale(Locale): "Khamis", "Jumaat", "Sabtu", - "Ahad ", + "Ahad", ] +class MalteseLocale(Locale): + + names = ["mt"] + + past = "{0} ilu" + future = "fi {0}" + and_word = "u" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "issa", + "second": "sekonda", + "seconds": "{0} sekondi", + "minute": "minuta", + "minutes": "{0} minuti", + "hour": "siegħa", + "hours": {"dual": "{0} sagħtejn", "plural": "{0} sigħat"}, + "day": "jum", + "days": {"dual": "{0} jumejn", "plural": "{0} ijiem"}, + "week": "ġimgħa", + "weeks": {"dual": "{0} ġimagħtejn", "plural": "{0} ġimgħat"}, + "month": "xahar", + "months": {"dual": "{0} xahrejn", "plural": "{0} xhur"}, + "year": "sena", + "years": {"dual": "{0} sentejn", "plural": "{0} snin"}, + } + + month_names = [ + "", + "Jannar", + "Frar", + "Marzu", + "April", + "Mejju", + "Ġunju", + "Lulju", + "Awwissu", + "Settembru", + "Ottubru", + "Novembru", + "Diċembru", + ] + + month_abbreviations = [ + "", + "Jan", + "Fr", + "Mar", + "Apr", + "Mejju", + "Ġun", + "Lul", + "Aw", + "Sett", + "Ott", + "Nov", + "Diċ", + ] + + day_names = [ + "", + "It-Tnejn", + "It-Tlieta", + "L-Erbgħa", + "Il-Ħamis", + "Il-Ġimgħa", + "Is-Sibt", + "Il-Ħadd", + ] + + day_abbreviations = [ + "", + "T", + "TL", + "E", + "Ħ", + "Ġ", + "S", + "Ħ", + ] + + def _format_timeframe( + self, timeframe: TimeFrameLiteral, delta: Union[float, int] + ) -> str: + form = self.timeframes[timeframe] + delta = abs(delta) + if isinstance(form, Mapping): + if delta == 2: + form = form["dual"] + else: + form = form["plural"] + + return form.format(delta) + + class OdiaLocale(Locale): names = ["or", "or-in"] diff --git a/tests/test_locales.py b/tests/test_locales.py index 7a9c3028a..c33f065c0 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -362,6 +362,33 @@ def test_format_relative_future(self): assert result == "ഒരു മണിക്കൂർ മുമ്പ്" +@pytest.mark.usefixtures("lang_locale") +class TestMalteseLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "issa" + assert self.locale._format_timeframe("second", 1) == "sekonda" + assert self.locale._format_timeframe("seconds", 30) == "30 sekondi" + assert self.locale._format_timeframe("minute", 1) == "minuta" + assert self.locale._format_timeframe("minutes", 4) == "4 minuti" + assert self.locale._format_timeframe("hour", 1) == "siegħa" + assert self.locale._format_timeframe("hours", 2) == "2 sagħtejn" + assert self.locale._format_timeframe("hours", 4) == "4 sigħat" + assert self.locale._format_timeframe("day", 1) == "jum" + assert self.locale._format_timeframe("days", 2) == "2 jumejn" + assert self.locale._format_timeframe("days", 5) == "5 ijiem" + assert self.locale._format_timeframe("month", 1) == "xahar" + assert self.locale._format_timeframe("months", 2) == "2 xahrejn" + assert self.locale._format_timeframe("months", 7) == "7 xhur" + assert self.locale._format_timeframe("year", 1) == "sena" + assert self.locale._format_timeframe("years", 2) == "2 sentejn" + assert self.locale._format_timeframe("years", 8) == "8 snin" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "Is-Sibt" + assert self.locale.day_abbreviation(dt.isoweekday()) == "S" + + @pytest.mark.usefixtures("lang_locale") class TestHindiLocale: def test_format_timeframe(self): From 05c1a1a5c8526f70a79b3e90fa15f9e030006171 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 9 May 2021 15:24:30 -0400 Subject: [PATCH 521/649] Update dependencies (#975) --- .pre-commit-config.yaml | 12 ++++++------ arrow/arrow.py | 2 +- arrow/locales.py | 2 +- requirements.txt | 8 ++++---- tests/test_parser.py | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e00406c8a..b2494fb9a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,16 +16,16 @@ repos: - id: check-merge-conflict - id: debug-statements - repo: https://github.com/timothycrosley/isort - rev: 5.7.0 + rev: 5.8.0 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.10.0 + rev: v2.15.0 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.7.1 + rev: v1.8.0 hooks: - id: python-no-eval - id: python-check-blanket-noqa @@ -34,16 +34,16 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 21.5b0 hooks: - id: black args: [--safe, --quiet, --target-version=py36] - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 + rev: 3.9.2 hooks: - id: flake8 additional_dependencies: [flake8-bugbear] - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.800' + rev: 'v0.812' hooks: - id: mypy diff --git a/arrow/arrow.py b/arrow/arrow.py index b962c6561..d01fa8943 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -905,7 +905,7 @@ def float_timestamp(self) -> float: @property def fold(self) -> int: - """ Returns the ``fold`` value of the :class:`Arrow ` object. """ + """Returns the ``fold`` value of the :class:`Arrow ` object.""" return self._datetime.fold diff --git a/arrow/locales.py b/arrow/locales.py index 6a3409bd8..b0b11c115 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -85,7 +85,7 @@ def get_locale_by_class_name(name: str) -> "Locale": class Locale: - """ Represents locale-specific data and functionality. """ + """Represents locale-specific data and functionality.""" names: ClassVar[List[str]] = [] diff --git a/requirements.txt b/requirements.txt index 002349a56..6e7dffdf5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ dateparser==1.0.* -pre-commit==2.10.* +pre-commit==2.12.* pytest==6.2.* pytest-cov==2.11.* -pytest-mock==3.5.* +pytest-mock==3.6.* python-dateutil==2.8.* pytz==2019.* simplejson==3.17.* -sphinx==3.5.* -sphinx-autodoc-typehints==1.11.* +sphinx==4.0.* +sphinx-autodoc-typehints==1.12.* diff --git a/tests/test_parser.py b/tests/test_parser.py index 1d5a541f4..c2f3fa9c9 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -280,7 +280,7 @@ def test_parse_expanded_timestamp(self): # "x" token should parse integer timestamps below MAX_TIMESTAMP normally self.expected = datetime.fromtimestamp(int(timestamp), tz=tz_utc) - assert self.parser.parse("{:d}".format(int(timestamp)), "x") == self.expected + assert self.parser.parse(f"{int(timestamp):d}", "x") == self.expected self.expected = datetime.fromtimestamp(round(timestamp, 3), tz=tz_utc) assert self.parser.parse(f"{timestamp_milli:d}", "x") == self.expected @@ -290,7 +290,7 @@ def test_parse_expanded_timestamp(self): # anything above max µs timestamp should fail with pytest.raises(ValueError): - self.parser.parse("{:d}".format(int(MAX_TIMESTAMP_US) + 1), "x") + self.parser.parse(f"{int(MAX_TIMESTAMP_US) + 1:d}", "x") # floats are not allowed with the "x" token with pytest.raises(ParserMatchError): From 119bb6730c87f3c95e4ebf6355ba817f58c71ee2 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 16 May 2021 17:31:09 -0400 Subject: [PATCH 522/649] Change requirements.txt to requirements-dev.txt and clean up dependency versioning. (#978) --- .github/workflows/continuous_integration.yml | 12 ++++++------ .pre-commit-config.yaml | 6 +++--- MANIFEST.in | 2 +- Makefile | 4 ++-- requirements-dev.txt | 10 ++++++++++ requirements.txt | 10 ---------- setup.cfg | 6 +----- setup.py | 10 +++++----- tox.ini | 2 +- 9 files changed, 29 insertions(+), 33 deletions(-) create mode 100644 requirements-dev.txt delete mode 100644 requirements.txt diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 350bbb4f5..c3de5ffea 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -33,7 +33,7 @@ jobs: if: startsWith(runner.os, 'Linux') with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} restore-keys: | ${{ runner.os }}-pip- @@ -42,7 +42,7 @@ jobs: if: startsWith(runner.os, 'macOS') with: path: ~/Library/Caches/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} restore-keys: | ${{ runner.os }}-pip- @@ -51,7 +51,7 @@ jobs: if: startsWith(runner.os, 'Windows') with: path: ~\AppData\Local\pip\Cache - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} restore-keys: | ${{ runner.os }}-pip- @@ -85,17 +85,17 @@ jobs: - uses: actions/checkout@v2 # Set up Python - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v2 with: - python-version: "3.8" + python-version: "3.9" # Configure pip cache - name: Cache pip uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} restore-keys: | ${{ runner.os }}-pip- diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b2494fb9a..9980187d6 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: v3.4.0 + rev: v4.0.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -20,7 +20,7 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.15.0 + rev: v2.16.0 hooks: - id: pyupgrade args: [--py36-plus] @@ -34,7 +34,7 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/psf/black - rev: 21.5b0 + rev: 21.5b1 hooks: - id: black args: [--safe, --quiet, --target-version=py36] diff --git a/MANIFEST.in b/MANIFEST.in index d9955ed96..8ac191e0e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include LICENSE CHANGELOG.rst README.rst Makefile requirements.txt tox.ini +include LICENSE CHANGELOG.rst README.rst Makefile requirements-dev.txt tox.ini recursive-include tests *.py recursive-include docs *.py *.rst *.bat Makefile diff --git a/Makefile b/Makefile index 18a7fbc5e..efdcb2d7b 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: auto test docs clean -auto: build38 +auto: build39 build36: PYTHON_VER = python3.6 build37: PYTHON_VER = python3.7 @@ -11,7 +11,7 @@ build36 build37 build38 build39: clean $(PYTHON_VER) -m venv venv . venv/bin/activate; \ pip install -U pip setuptools wheel; \ - pip install -r requirements.txt; \ + pip install -r requirements-dev.txt; \ pre-commit install test: diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 000000000..2035ec243 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,10 @@ +dateparser==1.* +pre-commit==2.* +pytest==6.* +pytest-cov==2.* +pytest-mock==3.* +python-dateutil==2.* +pytz==2019.* +simplejson==3.* +sphinx==4.* +sphinx-autodoc-typehints==1.* diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 6e7dffdf5..000000000 --- a/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -dateparser==1.0.* -pre-commit==2.12.* -pytest==6.2.* -pytest-cov==2.11.* -pytest-mock==3.6.* -python-dateutil==2.8.* -pytz==2019.* -simplejson==3.17.* -sphinx==4.0.* -sphinx-autodoc-typehints==1.12.* diff --git a/setup.cfg b/setup.cfg index bfb63588d..3add2419c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,12 +23,8 @@ warn_unreachable = True strict_equality = True no_implicit_reexport = True -## - allow_redefinition = True - -# Type annotation for test codes and migration files are not mandatory - +# Type annotations for testing code and migration files are not mandatory [mypy-*.tests.*,tests.*] ignore_errors = True diff --git a/setup.py b/setup.py index 14dff60db..df0a5a759 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,12 @@ # mypy: ignore-errors -from setuptools import setup +from pathlib import Path -with open("README.rst", encoding="utf-8") as f: - readme = f.read() +from setuptools import setup +readme = Path("README.rst").read_text(encoding="utf-8") +version = Path("arrow/_version.py").read_text(encoding="utf-8") about = {} -with open("arrow/_version.py", encoding="utf-8") as f: - exec(f.read(), about) +exec(version, about) setup( name="arrow", diff --git a/tox.ini b/tox.ini index a1ebbc93b..7113f0d95 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ python = 3.9: py39 [testenv] -deps = -rrequirements.txt +deps = -rrequirements-dev.txt allowlist_externals = pytest commands = pytest From e68df211f920e2a96d420f27bebd3be6d38c91ee Mon Sep 17 00:00:00 2001 From: adil Date: Sat, 29 May 2021 20:26:25 +0300 Subject: [PATCH 523/649] Update TurkishLocale --- arrow/locales.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index b0b11c115..2db0bd881 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2300,6 +2300,7 @@ class TurkishLocale(Locale): past = "{0} önce" future = "{0} sonra" + and_word = "ve" timeframes = { "now": "şimdi", @@ -2311,12 +2312,16 @@ class TurkishLocale(Locale): "hours": "{0} saat", "day": "bir gün", "days": "{0} gün", + "week": "hafta", + "weeks": "{0} hafta", "month": "bir ay", "months": "{0} ay", "year": "yıl", "years": "{0} yıl", } + meridians = {"am": "öö", "pm": "ös", "AM": "ÖÖ", "PM": "ÖS"} + month_names = [ "", "Ocak", From 2443773c0ed57819651a13e0d7f938dad325d7b8 Mon Sep 17 00:00:00 2001 From: adil Date: Sun, 30 May 2021 17:21:31 +0300 Subject: [PATCH 524/649] Add TestTurkishLocale --- tests/test_locales.py | 68 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/test_locales.py b/tests/test_locales.py index c33f065c0..f612b28a8 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1611,3 +1611,71 @@ def test_format_relative_future(self): result = self.locale._format_relative("ଏକ ଘଣ୍ଟା", "hour", -1) assert result == "ଏକ ଘଣ୍ଟା ପୂର୍ବେ" + + +@pytest.mark.usefixtures("lang_locale") +class TestTurkishLocale: + def test_singles_mk(self): + assert self.locale._format_timeframe("second", 1) == "bir saniye" + assert self.locale._format_timeframe("minute", 1) == "bir dakika" + assert self.locale._format_timeframe("hour", 1) == "bir saat" + assert self.locale._format_timeframe("day", 1) == "bir gün" + assert self.locale._format_timeframe("week", 1) == "hafta" + assert self.locale._format_timeframe("month", 1) == "bir ay" + assert self.locale._format_timeframe("year", 1) == "yıl" + + def test_meridians_mk(self): + assert self.locale.meridian(7, "A") == "ÖÖ" + assert self.locale.meridian(18, "A") == "ÖS" + assert self.locale.meridian(10, "a") == "öö" + assert self.locale.meridian(22, "a") == "ös" + + def test_describe_mk(self): + assert self.locale.describe("second", only_distance=True) == "bir saniye" + assert self.locale.describe("second", only_distance=False) == "bir saniye sonra" + assert self.locale.describe("minute", only_distance=True) == "bir dakika" + assert self.locale.describe("minute", only_distance=False) == "bir dakika sonra" + assert self.locale.describe("hour", only_distance=True) == "bir saat" + assert self.locale.describe("hour", only_distance=False) == "bir saat sonra" + assert self.locale.describe("day", only_distance=True) == "bir gün" + assert self.locale.describe("day", only_distance=False) == "bir gün sonra" + assert self.locale.describe("week", only_distance=True) == "hafta" + assert self.locale.describe("week", only_distance=False) == "hafta sonra" + assert self.locale.describe("month", only_distance=True) == "bir ay" + assert self.locale.describe("month", only_distance=False) == "bir ay sonra" + assert self.locale.describe("year", only_distance=True) == "yıl" + assert self.locale.describe("year", only_distance=False) == "yıl sonra" + + def test_relative_mk(self): + assert self.locale._format_relative("şimdi", "now", 0) == "şimdi" + assert self.locale._format_relative("1 saniye", "seconds", 1) == "1 saniye sonra" + assert self.locale._format_relative("1 saniye", "seconds", -1) == "1 saniye önce" + assert self.locale._format_relative("1 dakika", "minutes", 1) == "1 dakika sonra" + assert self.locale._format_relative("1 dakika", "minutes", -1) == "1 dakika önce" + assert self.locale._format_relative("1 saat", "hours", 1) == "1 saat sonra" + assert self.locale._format_relative("1 saat", "hours", -1) == "1 saat önce" + assert self.locale._format_relative("1 gün", "days", 1) == "1 gün sonra" + assert self.locale._format_relative("1 gün", "days", -1) == "1 gün önce" + assert self.locale._format_relative("1 hafta", "weeks", 1) == "1 hafta sonra" + assert self.locale._format_relative("1 hafta", "weeks", -1) == "1 hafta önce" + assert self.locale._format_relative("1 ay", "months", 1) == "1 ay sonra" + assert self.locale._format_relative("1 ay", "months", -1) == "1 ay önce" + assert self.locale._format_relative("1 yıl", "years", 1) == "1 yıl sonra" + assert self.locale._format_relative("1 yıl", "years", -1) == "1 yıl önce" + + def test_plurals_mk(self): + assert self.locale._format_timeframe("now", 0) == "şimdi" + assert self.locale._format_timeframe("second", 1) == "bir saniye" + assert self.locale._format_timeframe("seconds", 30) == "30 saniye" + assert self.locale._format_timeframe("minute", 1) == "bir dakika" + assert self.locale._format_timeframe("minutes", 40) == "40 dakika" + assert self.locale._format_timeframe("hour", 1) == "bir saat" + assert self.locale._format_timeframe("hours", 23) == "23 saat" + assert self.locale._format_timeframe("day", 1) == "bir gün" + assert self.locale._format_timeframe("days", 12) == "12 gün" + assert self.locale._format_timeframe("week", 1) == "hafta" + assert self.locale._format_timeframe("weeks", 38) == "38 hafta" + assert self.locale._format_timeframe("month", 1) == "bir ay" + assert self.locale._format_timeframe("months", 11) == "11 ay" + assert self.locale._format_timeframe("year", 1) == "yıl" + assert self.locale._format_timeframe("years", 12) == "12 yıl" From 7060ea253ddee951d896bf7bf7a6f95dbf6e31d4 Mon Sep 17 00:00:00 2001 From: adil Date: Mon, 31 May 2021 11:43:21 +0300 Subject: [PATCH 525/649] Fixed TurkishLocale --- arrow/locales.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 2db0bd881..669ab2f86 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2312,11 +2312,11 @@ class TurkishLocale(Locale): "hours": "{0} saat", "day": "bir gün", "days": "{0} gün", - "week": "hafta", + "week": "bir hafta", "weeks": "{0} hafta", "month": "bir ay", "months": "{0} ay", - "year": "yıl", + "year": "bir yıl", "years": "{0} yıl", } From 99c700e184d0bdd57e4b9571eecb78d4345d675b Mon Sep 17 00:00:00 2001 From: adil Date: Mon, 31 May 2021 11:43:40 +0300 Subject: [PATCH 526/649] Update TestTurkishLocale --- tests/test_locales.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_locales.py b/tests/test_locales.py index f612b28a8..306c5b421 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1620,9 +1620,9 @@ def test_singles_mk(self): assert self.locale._format_timeframe("minute", 1) == "bir dakika" assert self.locale._format_timeframe("hour", 1) == "bir saat" assert self.locale._format_timeframe("day", 1) == "bir gün" - assert self.locale._format_timeframe("week", 1) == "hafta" + assert self.locale._format_timeframe("week", 1) == "bir hafta" assert self.locale._format_timeframe("month", 1) == "bir ay" - assert self.locale._format_timeframe("year", 1) == "yıl" + assert self.locale._format_timeframe("year", 1) == "bir yıl" def test_meridians_mk(self): assert self.locale.meridian(7, "A") == "ÖÖ" @@ -1639,12 +1639,12 @@ def test_describe_mk(self): assert self.locale.describe("hour", only_distance=False) == "bir saat sonra" assert self.locale.describe("day", only_distance=True) == "bir gün" assert self.locale.describe("day", only_distance=False) == "bir gün sonra" - assert self.locale.describe("week", only_distance=True) == "hafta" - assert self.locale.describe("week", only_distance=False) == "hafta sonra" + assert self.locale.describe("week", only_distance=True) == "bir hafta" + assert self.locale.describe("week", only_distance=False) == "bir hafta sonra" assert self.locale.describe("month", only_distance=True) == "bir ay" assert self.locale.describe("month", only_distance=False) == "bir ay sonra" - assert self.locale.describe("year", only_distance=True) == "yıl" - assert self.locale.describe("year", only_distance=False) == "yıl sonra" + assert self.locale.describe("year", only_distance=True) == "bir yıl" + assert self.locale.describe("year", only_distance=False) == "bir yıl sonra" def test_relative_mk(self): assert self.locale._format_relative("şimdi", "now", 0) == "şimdi" @@ -1673,9 +1673,9 @@ def test_plurals_mk(self): assert self.locale._format_timeframe("hours", 23) == "23 saat" assert self.locale._format_timeframe("day", 1) == "bir gün" assert self.locale._format_timeframe("days", 12) == "12 gün" - assert self.locale._format_timeframe("week", 1) == "hafta" + 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 ay" assert self.locale._format_timeframe("months", 11) == "11 ay" - assert self.locale._format_timeframe("year", 1) == "yıl" + assert self.locale._format_timeframe("year", 1) == "bir yıl" assert self.locale._format_timeframe("years", 12) == "12 yıl" From 29c1e5413f07ebefa0bb312095a370cfd104984a Mon Sep 17 00:00:00 2001 From: adil Date: Tue, 1 Jun 2021 01:41:29 +0300 Subject: [PATCH 527/649] Fixed TestTurkishLocale --- tests/test_locales.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/test_locales.py b/tests/test_locales.py index 306c5b421..0cce3379d 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1648,10 +1648,18 @@ def test_describe_mk(self): def test_relative_mk(self): assert self.locale._format_relative("şimdi", "now", 0) == "şimdi" - assert self.locale._format_relative("1 saniye", "seconds", 1) == "1 saniye sonra" - assert self.locale._format_relative("1 saniye", "seconds", -1) == "1 saniye önce" - assert self.locale._format_relative("1 dakika", "minutes", 1) == "1 dakika sonra" - assert self.locale._format_relative("1 dakika", "minutes", -1) == "1 dakika önce" + assert ( + self.locale._format_relative("1 saniye", "seconds", 1) == "1 saniye sonra" + ) + assert ( + self.locale._format_relative("1 saniye", "seconds", -1) == "1 saniye önce" + ) + assert ( + self.locale._format_relative("1 dakika", "minutes", 1) == "1 dakika sonra" + ) + assert ( + self.locale._format_relative("1 dakika", "minutes", -1) == "1 dakika önce" + ) assert self.locale._format_relative("1 saat", "hours", 1) == "1 saat sonra" assert self.locale._format_relative("1 saat", "hours", -1) == "1 saat önce" assert self.locale._format_relative("1 gün", "days", 1) == "1 gün sonra" @@ -1661,7 +1669,7 @@ def test_relative_mk(self): assert self.locale._format_relative("1 ay", "months", 1) == "1 ay sonra" assert self.locale._format_relative("1 ay", "months", -1) == "1 ay önce" assert self.locale._format_relative("1 yıl", "years", 1) == "1 yıl sonra" - assert self.locale._format_relative("1 yıl", "years", -1) == "1 yıl önce" + assert self.locale._format_relative("1 yıl", "years", -1) == "1 yıl önce" def test_plurals_mk(self): assert self.locale._format_timeframe("now", 0) == "şimdi" From 197bd64a06ea4d9b89fae6050353bd1e059bc8f4 Mon Sep 17 00:00:00 2001 From: cyriaka90 Date: Fri, 11 Jun 2021 01:28:15 +0200 Subject: [PATCH 528/649] Add Serbian locale (#980) --- arrow/locales.py | 94 +++++++++++++++++++++++++++++++++++++++++++ tests/test_locales.py | 31 ++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 669ab2f86..bb9fc3533 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -4990,3 +4990,97 @@ def _ordinal_number(self, n: int) -> str: if n == 6: return f"{n}ଷ୍ଠ" return "" + + +class SerbianLocale(Locale): + + names = ["sr", "sr-sp"] + + past = "pre {0}" + future = "za {0}" + and_word = "i" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "sada", + "second": "sekundu", + "seconds": {"double": "{0} sekunde", "higher": "{0} sekundi"}, + "minute": "minutu", + "minutes": {"double": "{0} minute", "higher": "{0} minuta"}, + "hour": "sat", + "hours": {"double": "{0} sata", "higher": "{0} sati"}, + "day": "dan", + "days": {"double": "{0} dana", "higher": "{0} dana"}, + "week": "nedelju", + "weeks": {"double": "{0} nedelje", "higher": "{0} nedelja"}, + "month": "mesec", + "months": {"double": "{0} meseca", "higher": "{0} meseci"}, + "year": "godinu", + "years": {"double": "{0} godine", "higher": "{0} godina"}, + } + + month_names = [ + "", + "januar", # Јануар + "februar", # фебруар + "mart", # март + "april", # април + "maj", # мај + "juni", # јун + "juli", # јул + "avgust", # август + "septembar", # септембар + "oktobar", # октобар + "novembar", # новембар + "decembar", # децембар + ] + + month_abbreviations = [ + "", + "jan.", + "febr.", + "mart", + "april", + "maj", + "juni", + "juli", + "avg.", + "sept.", + "okt.", + "nov.", + "dec.", + ] + + day_names = [ + "", + "ponedeljak", # понедељак + "utorak", # уторак + "sreda", # среда + "četvrtak", # четвртак + "petak", # петак + "subota", # субота + "nedelja", # недеља + ] + + day_abbreviations = [ + "", + "po", # по + "ut", # ут + "sr", # ср + "če", # че + "pe", # пе + "su", # су + "ne", # не + ] + + def _format_timeframe( + self, timeframe: TimeFrameLiteral, delta: Union[float, int] + ) -> str: + form = self.timeframes[timeframe] + delta = abs(delta) + if isinstance(form, Mapping): + if 1 < delta <= 4: + form = form["double"] + else: + form = form["higher"] + + return form.format(delta) diff --git a/tests/test_locales.py b/tests/test_locales.py index 0cce3379d..2f59c0e3e 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1206,6 +1206,37 @@ def test_weekday(self): assert self.locale.day_abbreviation(dt.isoweekday()) == "su" +@pytest.mark.usefixtures("lang_locale") +class TestSerbianLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "sada" + assert self.locale._format_timeframe("second", 1) == "sekundu" + assert self.locale._format_timeframe("seconds", 3) == "3 sekunde" + assert self.locale._format_timeframe("seconds", 30) == "30 sekundi" + assert self.locale._format_timeframe("minute", 1) == "minutu" + assert self.locale._format_timeframe("minutes", 4) == "4 minute" + assert self.locale._format_timeframe("minutes", 40) == "40 minuta" + assert self.locale._format_timeframe("hour", 1) == "sat" + assert self.locale._format_timeframe("hours", 3) == "3 sata" + assert self.locale._format_timeframe("hours", 23) == "23 sati" + assert self.locale._format_timeframe("day", 1) == "dan" + assert self.locale._format_timeframe("days", 12) == "12 dana" + assert self.locale._format_timeframe("week", 1) == "nedelju" + assert self.locale._format_timeframe("weeks", 2) == "2 nedelje" + assert self.locale._format_timeframe("weeks", 11) == "11 nedelja" + assert self.locale._format_timeframe("month", 1) == "mesec" + assert self.locale._format_timeframe("months", 2) == "2 meseca" + assert self.locale._format_timeframe("months", 11) == "11 meseci" + assert self.locale._format_timeframe("year", 1) == "godinu" + assert self.locale._format_timeframe("years", 2) == "2 godine" + assert self.locale._format_timeframe("years", 12) == "12 godina" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "subota" + assert self.locale.day_abbreviation(dt.isoweekday()) == "su" + + @pytest.mark.usefixtures("lang_locale") class TestLatinLocale: def test_format_timeframe(self): From 7c9632c09161b1edb67fadb4bf8f3c1c0f5cb101 Mon Sep 17 00:00:00 2001 From: cyriaka90 Date: Mon, 14 Jun 2021 15:34:37 +0200 Subject: [PATCH 529/649] Add Sami locale (#989) --- arrow/constants.py | 4 +++ arrow/locales.py | 80 +++++++++++++++++++++++++++++++++++++++++++ tests/test_arrow.py | 4 +++ tests/test_locales.py | 22 ++++++++++++ 4 files changed, 110 insertions(+) diff --git a/arrow/constants.py b/arrow/constants.py index 5986b6104..795065e3c 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -59,6 +59,10 @@ "el-gr", "ja", "ja-jp", + "se", + "se-fi", + "se-no", + "se-se", "sv", "sv-se", "zh", diff --git a/arrow/locales.py b/arrow/locales.py index bb9fc3533..d0afa063a 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -4901,6 +4901,86 @@ def _format_timeframe( return form.format(delta) +class SamiLocale(Locale): + + names = ["se", "se-fi", "se-no", "se-se"] + + past = "{0} dassái" + future = "{0} " # NOTE: couldn't find preposition for Sami here, none needed? + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "dál", + "second": "sekunda", + "seconds": "{0} sekundda", + "minute": "minuhta", + "minutes": "{0} minuhta", + "hour": "diimmu", + "hours": "{0} diimmu", + "day": "beaivvi", + "days": "{0} beaivvi", + "week": "vahku", + "weeks": "{0} vahku", + "month": "mánu", + "months": "{0} mánu", + "year": "jagi", + "years": "{0} jagi", + } + + month_names = [ + "", + "Ođđajagimánnu", + "Guovvamánnu", + "Njukčamánnu", + "Cuoŋománnu", + "Miessemánnu", + "Geassemánnu", + "Suoidnemánnu", + "Borgemánnu", + "Čakčamánnu", + "Golggotmánnu", + "Skábmamánnu", + "Juovlamánnu", + ] + + month_abbreviations = [ + "", + "Ođđajagimánnu", + "Guovvamánnu", + "Njukčamánnu", + "Cuoŋománnu", + "Miessemánnu", + "Geassemánnu", + "Suoidnemánnu", + "Borgemánnu", + "Čakčamánnu", + "Golggotmánnu", + "Skábmamánnu", + "Juovlamánnu", + ] + + day_names = [ + "", + "Mánnodat", + "Disdat", + "Gaskavahkku", + "Duorastat", + "Bearjadat", + "Lávvordat", + "Sotnabeaivi", + ] + + day_abbreviations = [ + "", + "Mánnodat", + "Disdat", + "Gaskavahkku", + "Duorastat", + "Bearjadat", + "Lávvordat", + "Sotnabeaivi", + ] + + class OdiaLocale(Locale): names = ["or", "or-in"] diff --git a/tests/test_arrow.py b/tests/test_arrow.py index bc72bf317..724be99ac 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2382,6 +2382,10 @@ def locale_list_no_weeks(): "ms-bn", "or", "or-in", + "se", + "se-fi", + "se-no", + "se-se", ] return tested_langs diff --git a/tests/test_locales.py b/tests/test_locales.py index 2f59c0e3e..9b097bce9 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1307,6 +1307,28 @@ def test_weekday(self): assert self.locale.day_name(dt.isoweekday()) == "Sabtu" +@pytest.mark.usefixtures("lang_locale") +class TestSamiLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "dál" + assert self.locale._format_timeframe("second", 1) == "sekunda" + assert self.locale._format_timeframe("seconds", 3) == "3 sekundda" + assert self.locale._format_timeframe("minute", 1) == "minuhta" + assert self.locale._format_timeframe("minutes", 4) == "4 minuhta" + assert self.locale._format_timeframe("hour", 1) == "diimmu" + assert self.locale._format_timeframe("hours", 23) == "23 diimmu" + assert self.locale._format_timeframe("day", 1) == "beaivvi" + assert self.locale._format_timeframe("days", 12) == "12 beaivvi" + assert self.locale._format_timeframe("month", 1) == "mánu" + assert self.locale._format_timeframe("months", 2) == "2 mánu" + assert self.locale._format_timeframe("year", 1) == "jagi" + assert self.locale._format_timeframe("years", 2) == "2 jagi" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "Lávvordat" + + @pytest.mark.usefixtures("lang_locale") class TestEstonianLocale: def test_format_timeframe(self): From f1df58b321c43bc0caf6c1a669b3f814f9159bbc Mon Sep 17 00:00:00 2001 From: ChingYi-AX <82449718+ChingYi-AX@users.noreply.github.com> Date: Wed, 23 Jun 2021 21:03:45 +0200 Subject: [PATCH 530/649] Add Luxembourgish Locale (#993) --- arrow/constants.py | 2 + arrow/locales.py | 102 ++++++++++++++++++++++++++++++++++++++++++ tests/test_arrow.py | 4 ++ tests/test_locales.py | 25 +++++++++++ 4 files changed, 133 insertions(+) diff --git a/arrow/constants.py b/arrow/constants.py index 795065e3c..b0ad4f898 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -128,4 +128,6 @@ "ms-bn", "or", "or-in", + "lb", + "lb-lu", } diff --git a/arrow/locales.py b/arrow/locales.py index d0afa063a..d445f2e20 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -5164,3 +5164,105 @@ def _format_timeframe( form = form["higher"] return form.format(delta) + + +class LuxembourgishLocale(Locale): + + names = ["lb", "lb-lu"] + + past = "virun {0}" + future = "an {0}" + and_word = "an" + + timeframes = { + "now": "just elo", + "second": "enger Sekonn", + "seconds": "{0} Sekonnen", + "minute": "enger Minutt", + "minutes": "{0} Minutten", + "hour": "enger Stonn", + "hours": "{0} Stonnen", + "day": "engem Dag", + "days": "{0} Deeg", + "week": "enger Woch", + "weeks": "{0} Wochen", + "month": "engem Mount", + "months": "{0} Méint", + "year": "engem Joer", + "years": "{0} Jahren", + } + + timeframes_only_distance = timeframes.copy() + timeframes_only_distance["second"] = "eng Sekonn" + timeframes_only_distance["minute"] = "eng Minutt" + timeframes_only_distance["hour"] = "eng Stonn" + timeframes_only_distance["day"] = "een Dag" + timeframes_only_distance["days"] = "{0} Deeg" + timeframes_only_distance["week"] = "eng Woch" + timeframes_only_distance["month"] = "ee Mount" + timeframes_only_distance["months"] = "{0} Méint" + timeframes_only_distance["year"] = "ee Joer" + timeframes_only_distance["years"] = "{0} Joer" + + month_names = [ + "", + "Januar", + "Februar", + "Mäerz", + "Abrëll", + "Mee", + "Juni", + "Juli", + "August", + "September", + "Oktouber", + "November", + "Dezember", + ] + + month_abbreviations = [ + "", + "Jan", + "Feb", + "Mäe", + "Abr", + "Mee", + "Jun", + "Jul", + "Aug", + "Sep", + "Okt", + "Nov", + "Dez", + ] + + day_names = [ + "", + "Méindeg", + "Dënschdeg", + "Mëttwoch", + "Donneschdeg", + "Freideg", + "Samschdeg", + "Sonndeg", + ] + + day_abbreviations = ["", "Méi", "Dën", "Mët", "Don", "Fre", "Sam", "Son"] + + def _ordinal_number(self, n: int) -> str: + return f"{n}." + + def describe( + self, + timeframe: TimeFrameLiteral, + delta: Union[int, float] = 0, + only_distance: bool = False, + ) -> str: + + if not only_distance: + 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))) + + return humanized diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 724be99ac..2dab6ba34 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2386,6 +2386,8 @@ def locale_list_no_weeks(): "se-fi", "se-no", "se-se", + "lb", + "lb-lu", ] return tested_langs @@ -2441,6 +2443,8 @@ def locale_list_with_weeks(): "ms", "ms-my", "ms-bn", + "lb", + "lb-lu", ] return tested_langs diff --git a/tests/test_locales.py b/tests/test_locales.py index 9b097bce9..176980efd 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1740,3 +1740,28 @@ def test_plurals_mk(self): assert self.locale._format_timeframe("months", 11) == "11 ay" assert self.locale._format_timeframe("year", 1) == "bir yıl" assert self.locale._format_timeframe("years", 12) == "12 yıl" + + +@pytest.mark.usefixtures("lang_locale") +class TestLuxembourgishLocale: + def test_ordinal_number(self): + assert self.locale.ordinal_number(1) == "1." + + def test_define(self): + assert self.locale.describe("minute", only_distance=True) == "eng Minutt" + assert self.locale.describe("minute", only_distance=False) == "an enger Minutt" + assert self.locale.describe("hour", only_distance=True) == "eng Stonn" + assert self.locale.describe("hour", only_distance=False) == "an enger Stonn" + assert self.locale.describe("day", only_distance=True) == "een Dag" + assert self.locale.describe("day", only_distance=False) == "an engem Dag" + assert self.locale.describe("week", only_distance=True) == "eng Woch" + assert self.locale.describe("week", only_distance=False) == "an enger Woch" + assert self.locale.describe("month", only_distance=True) == "ee Mount" + assert self.locale.describe("month", only_distance=False) == "an engem Mount" + assert self.locale.describe("year", only_distance=True) == "ee Joer" + assert self.locale.describe("year", only_distance=False) == "an engem Joer" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "Samschdeg" + assert self.locale.day_abbreviation(dt.isoweekday()) == "Sam" From b8a9df754e81a2a5f2c8d5efd42d3567cca5e21c Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Wed, 23 Jun 2021 23:36:50 -0400 Subject: [PATCH 531/649] Fix floats appearing in Humanize output for locales that override _format_timeframe (#986) * Fixed floats in humanize bug * Simplified changes * Fixed typo in comments and test cases Co-authored-by: Jad Chaar --- arrow/locales.py | 9 +++++---- tests/test_arrow.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index d445f2e20..7ad396e8c 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -147,7 +147,7 @@ def describe( :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords """ - humanized = self._format_timeframe(timeframe, delta) + humanized = self._format_timeframe(timeframe, trunc(delta)) if not only_distance: humanized = self._format_relative(humanized, timeframe, delta) @@ -165,7 +165,8 @@ def describe_multi( """ parts = [ - self._format_timeframe(timeframe, delta) for timeframe, delta in timeframes + self._format_timeframe(timeframe, trunc(delta)) + for timeframe, delta in timeframes ] if self.and_word: parts.insert(-1, self.and_word) @@ -3318,7 +3319,7 @@ def _format_timeframe( else: key = timeframe - return self.timeframes[key].format(trunc(abs(delta))) + return self.timeframes[key].format(abs(delta)) def describe_multi( self, @@ -3334,7 +3335,7 @@ def describe_multi( humanized = "" for index, (timeframe, delta) in enumerate(timeframes): - last_humanized = self._format_timeframe(timeframe, delta) + last_humanized = self._format_timeframe(timeframe, trunc(delta)) if index == 0: humanized = last_humanized elif index == len(timeframes) - 1: # Must have at least 2 items diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 2dab6ba34..ba251270f 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2266,6 +2266,25 @@ def test_untranslated_granularity(self, mocker): with pytest.raises(ValueError): arw.humanize(later, granularity="week") + # 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 + # is truncated on call + + def test_no_floats(self): + arw = arrow.Arrow(2013, 1, 1, 0, 0, 0) + later = arw.shift(seconds=55000) + humanize_string = arw.humanize(later, locale="bg", granularity="minute") + assert humanize_string == "916 минути назад" + + def test_no_floats_multi_gran(self): + arw = arrow.Arrow(2013, 1, 1, 0, 0, 0) + later = arw.shift(seconds=55000) + humanize_string = arw.humanize( + later, locale="bg", granularity=["second", "minute"] + ) + assert humanize_string == "916 минути 40 няколко секунди назад" + @pytest.mark.usefixtures("time_2013_01_01") class TestArrowHumanizeTestsWithLocale: From 57332ed039a974ddeee566790d14c0e28cdb3888 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 24 Jun 2021 10:40:59 -0400 Subject: [PATCH 532/649] Bump version and update CHANGELOG. (#994) --- CHANGELOG.rst | 9 +++++++++ arrow/_version.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fe1cf1fee..5b0fb7df3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog ========= +1.1.1 (2021-06-24) +------------------ + +- [NEW] Added Odia, Maltese, Serbian, Sami, and Luxembourgish locales. +- [FIXED] All calls to ``arrow.get()`` should now properly pass the ``tzinfo`` argument to the Arrow constructor. See PR `#968 `_ for more info. +- [FIXED] Humanize output is now properly truncated when a locale class overrides ``_format_timeframe()``. +- [CHANGED] Renamed ``requirements.txt`` to ``requirements-dev.txt`` to prevent confusion with the dependencies in ``setup.py``. +- [CHANGED] Updated Turkish locale and added tests. + 1.1.0 (2021-04-26) ------------------ diff --git a/arrow/_version.py b/arrow/_version.py index 6849410aa..a82b376d2 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "1.1.0" +__version__ = "1.1.1" From 989ab94e4e51a3fe4abaa38897e0478e9e3ba736 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 27 Jun 2021 10:41:05 -0400 Subject: [PATCH 533/649] Improve _format_timeframe type annotations and update pre-commit dependencies. (#995) --- .pre-commit-config.yaml | 11 ++++----- arrow/locales.py | 50 +++++++++++------------------------------ 2 files changed, 19 insertions(+), 42 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9980187d6..7bc7ba027 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,16 +16,16 @@ repos: - id: check-merge-conflict - id: debug-statements - repo: https://github.com/timothycrosley/isort - rev: 5.8.0 + rev: 5.9.1 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.16.0 + rev: v2.19.4 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.8.0 + rev: v1.9.0 hooks: - id: python-no-eval - id: python-check-blanket-noqa @@ -34,7 +34,7 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/psf/black - rev: 21.5b1 + rev: 21.6b0 hooks: - id: black args: [--safe, --quiet, --target-version=py36] @@ -44,6 +44,7 @@ repos: - id: flake8 additional_dependencies: [flake8-bugbear] - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.812' + rev: 'v0.910' hooks: - id: mypy + additional_dependencies: [types-python-dateutil] diff --git a/arrow/locales.py b/arrow/locales.py index 7ad396e8c..30221fda3 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -268,9 +268,7 @@ def _ordinal_number(self, n: int) -> str: def _name_to_ordinal(self, lst: Sequence[str]) -> Dict[str, int]: return {elem.lower(): i for i, elem in enumerate(lst[1:], 1)} - def _format_timeframe( - self, timeframe: TimeFrameLiteral, delta: Union[float, int] - ) -> str: + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: # TODO: remove cast return cast(str, self.timeframes[timeframe]).format(trunc(abs(delta))) @@ -927,7 +925,7 @@ class FinnishLocale(Locale): day_abbreviations = ["", "ma", "ti", "ke", "to", "pe", "la", "su"] # TODO: Fix return type - def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: Union[float, int]) -> Tuple[str, str]: # type: ignore + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> Tuple[str, str]: # type: ignore return ( self.timeframes[timeframe][0].format(abs(delta)), self.timeframes[timeframe][1].format(abs(delta)), @@ -1302,9 +1300,7 @@ class DutchLocale(Locale): class SlavicBaseLocale(Locale): timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, List[str]]]] - def _format_timeframe( - self, timeframe: TimeFrameLiteral, delta: Union[float, int] - ) -> str: + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: form = self.timeframes[timeframe] delta = abs(delta) @@ -2518,9 +2514,7 @@ class ArabicLocale(Locale): ] day_abbreviations = ["", "إثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت", "أحد"] - def _format_timeframe( - self, timeframe: TimeFrameLiteral, delta: Union[float, int] - ) -> str: + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: form = self.timeframes[timeframe] delta = abs(delta) if isinstance(form, Mapping): @@ -2671,9 +2665,7 @@ class MoroccoArabicLocale(ArabicLocale): class IcelandicLocale(Locale): - def _format_timeframe( - self, timeframe: TimeFrameLiteral, delta: Union[float, int] - ) -> str: + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: form = self.timeframes[timeframe] if delta < 0: form = form[0] @@ -3030,9 +3022,7 @@ class CzechLocale(Locale): ] day_abbreviations = ["", "po", "út", "st", "čt", "pá", "so", "ne"] - def _format_timeframe( - self, timeframe: TimeFrameLiteral, delta: Union[float, int] - ) -> str: + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: """Czech aware time frame format function, takes into account the differences between past and future forms.""" abs_delta = abs(delta) @@ -3130,9 +3120,7 @@ class SlovakLocale(Locale): ] day_abbreviations = ["", "po", "ut", "st", "št", "pi", "so", "ne"] - def _format_timeframe( - self, timeframe: TimeFrameLiteral, delta: Union[float, int] - ) -> str: + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: """Slovak aware time frame format function, takes into account the differences between past and future forms.""" abs_delta = abs(delta) @@ -3306,9 +3294,7 @@ class HebrewLocale(Locale): day_names = ["", "שני", "שלישי", "רביעי", "חמישי", "שישי", "שבת", "ראשון"] day_abbreviations = ["", "ב׳", "ג׳", "ד׳", "ה׳", "ו׳", "ש׳", "א׳"] - def _format_timeframe( - self, timeframe: TimeFrameLiteral, delta: Union[float, int] - ) -> str: + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: """Hebrew couple of aware""" couple = f"2-{timeframe}" single = timeframe.rstrip("s") @@ -3627,9 +3613,7 @@ class HungarianLocale(Locale): meridians = {"am": "de", "pm": "du", "AM": "DE", "PM": "DU"} - def _format_timeframe( - self, timeframe: TimeFrameLiteral, delta: Union[float, int] - ) -> str: + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: form = self.timeframes[timeframe] if isinstance(form, Mapping): @@ -4294,9 +4278,7 @@ class EstonianLocale(Locale): ] day_abbreviations = ["", "Esm", "Teis", "Kolm", "Nelj", "Re", "Lau", "Püh"] - def _format_timeframe( - self, timeframe: TimeFrameLiteral, delta: Union[float, int] - ) -> str: + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: form = self.timeframes[timeframe] if delta > 0: _form = form["future"] @@ -4551,9 +4533,7 @@ class CroatianLocale(Locale): "ne", ] - def _format_timeframe( - self, timeframe: TimeFrameLiteral, delta: Union[float, int] - ) -> str: + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: form = self.timeframes[timeframe] delta = abs(delta) if isinstance(form, Mapping): @@ -4888,9 +4868,7 @@ class MalteseLocale(Locale): "Ħ", ] - def _format_timeframe( - self, timeframe: TimeFrameLiteral, delta: Union[float, int] - ) -> str: + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: form = self.timeframes[timeframe] delta = abs(delta) if isinstance(form, Mapping): @@ -5153,9 +5131,7 @@ class SerbianLocale(Locale): "ne", # не ] - def _format_timeframe( - self, timeframe: TimeFrameLiteral, delta: Union[float, int] - ) -> str: + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: form = self.timeframes[timeframe] delta = abs(delta) if isinstance(form, Mapping): From aa0e805388d5b9ba634423c8a966e6bc45d14808 Mon Sep 17 00:00:00 2001 From: cyriaka90 Date: Tue, 29 Jun 2021 00:20:51 +0200 Subject: [PATCH 534/649] Expand locale name variants (#998) --- arrow/locales.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 30221fda3..a8500c413 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2886,7 +2886,7 @@ class MalayalamLocale(Locale): class HindiLocale(Locale): - names = ["hi"] + names = ["hi", "hi-in"] past = "{0} पहले" future = "{0} बाद" @@ -4790,7 +4790,7 @@ class MalayLocale(Locale): class MalteseLocale(Locale): - names = ["mt"] + names = ["mt", "mt-mt"] past = "{0} ilu" future = "fi {0}" @@ -5053,7 +5053,7 @@ def _ordinal_number(self, n: int) -> str: class SerbianLocale(Locale): - names = ["sr", "sr-sp"] + names = ["sr", "sr-rs", "sr-sp"] past = "pre {0}" future = "za {0}" From 37b45daccb956d1955d1b169a98df93f7e6da97c Mon Sep 17 00:00:00 2001 From: cyriaka90 Date: Wed, 7 Jul 2021 16:16:30 +0200 Subject: [PATCH 535/649] Add Zulu locale (#1002) --- arrow/locales.py | 98 +++++++++++++++++++++++++++++++++++++++++++ tests/test_locales.py | 23 ++++++++++ 2 files changed, 121 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index a8500c413..09a027ccb 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -5243,3 +5243,101 @@ def describe( humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) return humanized + + +class ZuluLocale(Locale): + + names = ["zu", "zu-za"] + + past = "{0} edlule" + future = "{0} " + and_word = "futhi" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[Mapping[str, str], str]]] = { + "now": "manje", + "second": {"past": "umzuzwana", "future": "ngomzuzwana"}, + "seconds": {"past": "{0} imizuzwana", "future": "{0} ngemizuzwana"}, + "minute": {"past": "umzuzu", "future": "ngomzuzu"}, + "minutes": {"past": "{0} imizuzu", "future": "{0} ngemizuzu"}, + "hour": {"past": "ihora", "future": "ngehora"}, + "hours": {"past": "{0} amahora", "future": "{0} emahoreni"}, + "day": {"past": "usuku", "future": "ngosuku"}, + "days": {"past": "{0} izinsuku", "future": "{0} ezinsukwini"}, + "week": {"past": "isonto", "future": "ngesonto"}, + "weeks": {"past": "{0} amasonto", "future": "{0} emasontweni"}, + "month": {"past": "inyanga", "future": "ngenyanga"}, + "months": {"past": "{0} izinyanga", "future": "{0} ezinyangeni"}, + "year": {"past": "unyaka", "future": "ngonyak"}, + "years": {"past": "{0} iminyaka", "future": "{0} eminyakeni"}, + } + + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + """Zulu aware time frame format function, takes into account + the differences between past and future forms.""" + abs_delta = abs(delta) + form = self.timeframes[timeframe] + + if isinstance(form, str): + return form.format(abs_delta) + + if delta > 0: + key = "future" + else: + key = "past" + form = form[key] + + return form.format(abs_delta) + + month_names = [ + "", + "uMasingane", + "uNhlolanja", + "uNdasa", + "UMbasa", + "UNhlaba", + "UNhlangulana", + "uNtulikazi", + "UNcwaba", + "uMandulo", + "uMfumfu", + "uLwezi", + "uZibandlela", + ] + + month_abbreviations = [ + "", + "uMasingane", + "uNhlolanja", + "uNdasa", + "UMbasa", + "UNhlaba", + "UNhlangulana", + "uNtulikazi", + "UNcwaba", + "uMandulo", + "uMfumfu", + "uLwezi", + "uZibandlela", + ] + + day_names = [ + "", + "uMsombuluko", + "uLwesibili", + "uLwesithathu", + "uLwesine", + "uLwesihlanu", + "uMgqibelo", + "iSonto", + ] + + day_abbreviations = [ + "", + "uMsombuluko", + "uLwesibili", + "uLwesithathu", + "uLwesine", + "uLwesihlanu", + "uMgqibelo", + "iSonto", + ] diff --git a/tests/test_locales.py b/tests/test_locales.py index 176980efd..3e74c15c4 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1329,6 +1329,29 @@ def test_weekday(self): assert self.locale.day_name(dt.isoweekday()) == "Lávvordat" +@pytest.mark.usefixtures("lang_locale") +class TestZuluLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "manje" + assert self.locale._format_timeframe("second", -1) == "umzuzwana" + assert self.locale._format_timeframe("second", 1) == "ngomzuzwana" + assert self.locale._format_timeframe("seconds", -3) == "3 imizuzwana" + assert self.locale._format_timeframe("minute", -1) == "umzuzu" + assert self.locale._format_timeframe("minutes", -4) == "4 imizuzu" + assert self.locale._format_timeframe("hour", -1) == "ihora" + assert self.locale._format_timeframe("hours", -23) == "23 amahora" + assert self.locale._format_timeframe("day", -1) == "usuku" + assert self.locale._format_timeframe("days", -12) == "12 izinsuku" + assert self.locale._format_timeframe("month", -1) == "inyanga" + assert self.locale._format_timeframe("months", -2) == "2 izinyanga" + assert self.locale._format_timeframe("year", -1) == "unyaka" + assert self.locale._format_timeframe("years", -2) == "2 iminyaka" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "uMgqibelo" + + @pytest.mark.usefixtures("lang_locale") class TestEstonianLocale: def test_format_timeframe(self): From c53113a2a3761529f8eca560a659b7e0e82f0795 Mon Sep 17 00:00:00 2001 From: ChingYi-AX <82449718+ChingYi-AX@users.noreply.github.com> Date: Fri, 9 Jul 2021 06:35:14 +0200 Subject: [PATCH 536/649] Add Tamil locale (#1004) --- arrow/constants.py | 3 ++ arrow/locales.py | 88 +++++++++++++++++++++++++++++++++++++++++++ tests/test_arrow.py | 6 +++ tests/test_locales.py | 44 ++++++++++++++++++++++ 4 files changed, 141 insertions(+) diff --git a/arrow/constants.py b/arrow/constants.py index b0ad4f898..53be6e8c5 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -130,4 +130,7 @@ "or-in", "lb", "lb-lu", + "ta", + "ta-in", + "ta-lk", } diff --git a/arrow/locales.py b/arrow/locales.py index 09a027ccb..81a494ff6 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -5341,3 +5341,91 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: "uMgqibelo", "iSonto", ] + + +class TamilLocale(Locale): + + names = ["ta", "ta-in", "ta-lk"] + + 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 = [ + "", + "சித்திரை", + "வைகாசி", + "ஆனி", + "ஆடி", + "ஆவணி", + "புரட்டாசி", + "ஐப்பசி", + "கார்த்திகை", + "மார்கழி", + "தை", + "மாசி", + "பங்குனி", + ] + + month_abbreviations = [ + "", + "ஜன", + "பிப்", + "மார்", + "ஏப்", + "மே", + "ஜூன்", + "ஜூலை", + "ஆக", + "செப்", + "அக்", + "நவ", + "டிச", + ] + + day_names = [ + "", + "திங்கட்கிழமை", + "செவ்வாய்க்கிழமை", + "புதன்கிழமை", + "வியாழக்கிழமை", + "வெள்ளிக்கிழமை", + "சனிக்கிழமை", + "ஞாயிற்றுக்கிழமை", + ] + + day_abbreviations = [ + "", + "திங்கட்", + "செவ்வாய்", + "புதன்", + "வியாழன்", + "வெள்ளி", + "சனி", + "ஞாயிறு", + ] + + def _ordinal_number(self, n: int) -> str: + if n == 1: + return f"{n}வது" + elif n >= 0: + return f"{n}ஆம்" + else: + return "" diff --git a/tests/test_arrow.py b/tests/test_arrow.py index ba251270f..cef9ee6db 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2407,6 +2407,9 @@ def locale_list_no_weeks(): "se-se", "lb", "lb-lu", + "ta", + "ta-in", + "ta-lk", ] return tested_langs @@ -2464,6 +2467,9 @@ def locale_list_with_weeks(): "ms-bn", "lb", "lb-lu", + "ta", + "ta-in", + "ta-lk", ] return tested_langs diff --git a/tests/test_locales.py b/tests/test_locales.py index 3e74c15c4..f25ba9a2e 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1788,3 +1788,47 @@ def test_weekday(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) assert self.locale.day_name(dt.isoweekday()) == "Samschdeg" assert self.locale.day_abbreviation(dt.isoweekday()) == "Sam" + + +@pytest.mark.usefixtures("lang_locale") +class TestTamilLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "இப்போது" + assert self.locale._format_timeframe("second", 1) == "ஒரு இரண்டாவது" + assert self.locale._format_timeframe("seconds", 3) == "3 விநாடிகள்" + assert self.locale._format_timeframe("minute", 1) == "ஒரு நிமிடம்" + assert self.locale._format_timeframe("minutes", 4) == "4 நிமிடங்கள்" + 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("week", 1) == "ஒரு வாரம்" + assert self.locale._format_timeframe("weeks", 12) == "12 வாரங்கள்" + assert self.locale._format_timeframe("month", 1) == "ஒரு மாதம்" + assert self.locale._format_timeframe("months", 2) == "2 மாதங்கள்" + assert self.locale._format_timeframe("year", 1) == "ஒரு ஆண்டு" + assert self.locale._format_timeframe("years", 2) == "2 ஆண்டுகள்" + + def test_ordinal_number(self): + assert self.locale._ordinal_number(0) == "0ஆம்" + assert self.locale._ordinal_number(1) == "1வது" + assert self.locale._ordinal_number(3) == "3ஆம்" + assert self.locale._ordinal_number(11) == "11ஆம்" + assert self.locale._ordinal_number(-1) == "" + + 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 == "ஒரு மணி நேரத்திற்கு முன்பு" + + 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 255f717c58bee638c02cf3a496f9daa53a2727a2 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 15 Jul 2021 00:46:09 -0400 Subject: [PATCH 537/649] Require dateutil 2.8.2+ (#1006) --- .pre-commit-config.yaml | 4 ++-- requirements-dev.txt | 5 +++-- setup.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7bc7ba027..ac22f4c2c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,11 +16,11 @@ repos: - id: check-merge-conflict - id: debug-statements - repo: https://github.com/timothycrosley/isort - rev: 5.9.1 + rev: 5.9.2 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.19.4 + rev: v2.21.0 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/requirements-dev.txt b/requirements-dev.txt index 2035ec243..84acf695b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,8 +3,9 @@ pre-commit==2.* pytest==6.* pytest-cov==2.* pytest-mock==3.* -python-dateutil==2.* -pytz==2019.* +python-dateutil>=2.8.2 +pytz==2021.* simplejson==3.* sphinx==4.* sphinx-autodoc-typehints==1.* +typing_extensions; python_version < '3.8' diff --git a/setup.py b/setup.py index df0a5a759..65b52b7dc 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ zip_safe=False, python_requires=">=3.6", install_requires=[ - "python-dateutil>=2.7.0", + "python-dateutil>=2.8.2", "typing_extensions; python_version<'3.8'", ], classifiers=[ From fe1afe2c2b28661e1fd67f382f08b924538b138a Mon Sep 17 00:00:00 2001 From: ALee008 <39236016+ALee008@users.noreply.github.com> Date: Fri, 30 Jul 2021 04:07:55 +0200 Subject: [PATCH 538/649] Parser Token Validation (#1007) --- arrow/parser.py | 5 ++++- tests/test_parser.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/arrow/parser.py b/arrow/parser.py index a1798a59a..e95d78b0d 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -574,9 +574,12 @@ def _parse_token( elif token in ["a", "A"]: if value in (self.locale.meridians["am"], self.locale.meridians["AM"]): parts["am_pm"] = "am" + if "hour" in parts and not 0 <= parts["hour"] <= 12: + raise ParserMatchError( + f"Hour token value must be between 0 and 12 inclusive for token {token!r}." + ) elif value in (self.locale.meridians["pm"], self.locale.meridians["PM"]): parts["am_pm"] = "pm" - elif token == "W": parts["weekdate"] = value diff --git a/tests/test_parser.py b/tests/test_parser.py index c2f3fa9c9..4552f65a2 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -210,6 +210,11 @@ def test_parse_numbers(self): == self.expected ) + 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) From 95838a8fab4661ebaa085e0c9449733d82b3c1d7 Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Sun, 1 Aug 2021 14:57:26 -0700 Subject: [PATCH 539/649] Refactor of Thai Locale for Dehumanize Support (#990) --- arrow/locales.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 81a494ff6..ce8d95672 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -3701,8 +3701,8 @@ class ThaiLocale(Locale): names = ["th", "th-th"] - past = "{0}{1}ที่ผ่านมา" - future = "ในอีก{1}{0}" + past = "{0} ที่ผ่านมา" + future = "ในอีก {0}" timeframes = { "now": "ขณะนี้", @@ -3777,10 +3777,14 @@ def _format_relative( """Thai normally doesn't have any space between words""" if timeframe == "now": return humanized - space = "" if timeframe == "seconds" else " " + direction = self.past if delta < 0 else self.future + relative_string = direction.format(humanized) + + if timeframe == "seconds": + relative_string = relative_string.replace(" ", "") - return direction.format(humanized, space) + return relative_string class BengaliLocale(Locale): From 326c62ce03b7319c1f320dee4e05a175ff8507f6 Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Sun, 1 Aug 2021 15:09:16 -0700 Subject: [PATCH 540/649] Refactor of Slavic Locales for Future Dehumanize Support (#992) --- arrow/locales.py | 178 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 133 insertions(+), 45 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index ce8d95672..5a2eaa366 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1298,19 +1298,19 @@ class DutchLocale(Locale): class SlavicBaseLocale(Locale): - timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, List[str]]]] + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: form = self.timeframes[timeframe] delta = abs(delta) - if isinstance(form, list): + if isinstance(form, Mapping): if delta % 10 == 1 and delta % 100 != 11: - form = form[0] + form = form["singular"] elif 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): - form = form[1] + form = form["dual"] else: - form = form[2] + form = form["plural"] return form.format(delta) @@ -1322,20 +1322,32 @@ class BelarusianLocale(SlavicBaseLocale): past = "{0} таму" future = "праз {0}" - timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, List[str]]]] = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "зараз", "second": "секунду", "seconds": "{0} некалькі секунд", "minute": "хвіліну", - "minutes": ["{0} хвіліну", "{0} хвіліны", "{0} хвілін"], + "minutes": { + "singular": "{0} хвіліну", + "dual": "{0} хвіліны", + "plural": "{0} хвілін", + }, "hour": "гадзіну", - "hours": ["{0} гадзіну", "{0} гадзіны", "{0} гадзін"], + "hours": { + "singular": "{0} гадзіну", + "dual": "{0} гадзіны", + "plural": "{0} гадзін", + }, "day": "дзень", - "days": ["{0} дзень", "{0} дні", "{0} дзён"], + "days": {"singular": "{0} дзень", "dual": "{0} дні", "plural": "{0} дзён"}, "month": "месяц", - "months": ["{0} месяц", "{0} месяцы", "{0} месяцаў"], + "months": { + "singular": "{0} месяц", + "dual": "{0} месяцы", + "plural": "{0} месяцаў", + }, "year": "год", - "years": ["{0} год", "{0} гады", "{0} гадоў"], + "years": {"singular": "{0} год", "dual": "{0} гады", "plural": "{0} гадоў"}, } month_names = [ @@ -1391,22 +1403,42 @@ class PolishLocale(SlavicBaseLocale): # The nouns should be in genitive case (Polish: "dopełniacz") # in order to correctly form `past` & `future` expressions. - timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, List[str]]]] = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "teraz", "second": "sekundę", - "seconds": ["{0} sekund", "{0} sekundy", "{0} sekund"], + "seconds": { + "singular": "{0} sekund", + "dual": "{0} sekundy", + "plural": "{0} sekund", + }, "minute": "minutę", - "minutes": ["{0} minut", "{0} minuty", "{0} minut"], + "minutes": { + "singular": "{0} minut", + "dual": "{0} minuty", + "plural": "{0} minut", + }, "hour": "godzinę", - "hours": ["{0} godzin", "{0} godziny", "{0} godzin"], + "hours": { + "singular": "{0} godzin", + "dual": "{0} godziny", + "plural": "{0} godzin", + }, "day": "dzień", "days": "{0} dni", "week": "tydzień", - "weeks": ["{0} tygodni", "{0} tygodnie", "{0} tygodni"], + "weeks": { + "singular": "{0} tygodni", + "dual": "{0} tygodnie", + "plural": "{0} tygodni", + }, "month": "miesiąc", - "months": ["{0} miesięcy", "{0} miesiące", "{0} miesięcy"], + "months": { + "singular": "{0} miesięcy", + "dual": "{0} miesiące", + "plural": "{0} miesięcy", + }, "year": "rok", - "years": ["{0} lat", "{0} lata", "{0} lat"], + "years": {"singular": "{0} lat", "dual": "{0} lata", "plural": "{0} lat"}, } month_names = [ @@ -1460,22 +1492,34 @@ class RussianLocale(SlavicBaseLocale): past = "{0} назад" future = "через {0}" - timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, List[str]]]] = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "сейчас", "second": "Второй", "seconds": "{0} несколько секунд", "minute": "минуту", - "minutes": ["{0} минуту", "{0} минуты", "{0} минут"], + "minutes": { + "singular": "{0} минуту", + "dual": "{0} минуты", + "plural": "{0} минут", + }, "hour": "час", - "hours": ["{0} час", "{0} часа", "{0} часов"], + "hours": {"singular": "{0} час", "dual": "{0} часа", "plural": "{0} часов"}, "day": "день", - "days": ["{0} день", "{0} дня", "{0} дней"], + "days": {"singular": "{0} день", "dual": "{0} дня", "plural": "{0} дней"}, "week": "неделю", - "weeks": ["{0} неделю", "{0} недели", "{0} недель"], + "weeks": { + "singular": "{0} неделю", + "dual": "{0} недели", + "plural": "{0} недель", + }, "month": "месяц", - "months": ["{0} месяц", "{0} месяца", "{0} месяцев"], + "months": { + "singular": "{0} месяц", + "dual": "{0} месяца", + "plural": "{0} месяцев", + }, "year": "год", - "years": ["{0} год", "{0} года", "{0} лет"], + "years": {"singular": "{0} год", "dual": "{0} года", "plural": "{0} лет"}, } month_names = [ @@ -1596,20 +1640,32 @@ class BulgarianLocale(SlavicBaseLocale): past = "{0} назад" future = "напред {0}" - timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, List[str]]]] = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "сега", "second": "секунда", "seconds": "{0} няколко секунди", "minute": "минута", - "minutes": ["{0} минута", "{0} минути", "{0} минути"], + "minutes": { + "singular": "{0} минута", + "dual": "{0} минути", + "plural": "{0} минути", + }, "hour": "час", - "hours": ["{0} час", "{0} часа", "{0} часа"], + "hours": {"singular": "{0} час", "dual": "{0} часа", "plural": "{0} часа"}, "day": "ден", - "days": ["{0} ден", "{0} дни", "{0} дни"], + "days": {"singular": "{0} ден", "dual": "{0} дни", "plural": "{0} дни"}, "month": "месец", - "months": ["{0} месец", "{0} месеца", "{0} месеца"], + "months": { + "singular": "{0} месец", + "dual": "{0} месеца", + "plural": "{0} месеца", + }, "year": "година", - "years": ["{0} година", "{0} години", "{0} години"], + "years": { + "singular": "{0} година", + "dual": "{0} години", + "plural": "{0} години", + }, } month_names = [ @@ -1663,20 +1719,32 @@ class UkrainianLocale(SlavicBaseLocale): past = "{0} тому" future = "за {0}" - timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, List[str]]]] = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "зараз", "second": "секунда", "seconds": "{0} кілька секунд", "minute": "хвилину", - "minutes": ["{0} хвилину", "{0} хвилини", "{0} хвилин"], + "minutes": { + "singular": "{0} хвилину", + "dual": "{0} хвилини", + "plural": "{0} хвилин", + }, "hour": "годину", - "hours": ["{0} годину", "{0} години", "{0} годин"], + "hours": { + "singular": "{0} годину", + "dual": "{0} години", + "plural": "{0} годин", + }, "day": "день", - "days": ["{0} день", "{0} дні", "{0} днів"], + "days": {"singular": "{0} день", "dual": "{0} дні", "plural": "{0} днів"}, "month": "місяць", - "months": ["{0} місяць", "{0} місяці", "{0} місяців"], + "months": { + "singular": "{0} місяць", + "dual": "{0} місяці", + "plural": "{0} місяців", + }, "year": "рік", - "years": ["{0} рік", "{0} роки", "{0} років"], + "years": {"singular": "{0} рік", "dual": "{0} роки", "plural": "{0} років"}, } month_names = [ @@ -1729,22 +1797,42 @@ class MacedonianLocale(SlavicBaseLocale): past = "пред {0}" future = "за {0}" - timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, List[str]]]] = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "сега", "second": "една секунда", - "seconds": ["{0} секунда", "{0} секунди", "{0} секунди"], + "seconds": { + "singular": "{0} секунда", + "dual": "{0} секунди", + "plural": "{0} секунди", + }, "minute": "една минута", - "minutes": ["{0} минута", "{0} минути", "{0} минути"], + "minutes": { + "singular": "{0} минута", + "dual": "{0} минути", + "plural": "{0} минути", + }, "hour": "еден саат", - "hours": ["{0} саат", "{0} саати", "{0} саати"], + "hours": {"singular": "{0} саат", "dual": "{0} саати", "plural": "{0} саати"}, "day": "еден ден", - "days": ["{0} ден", "{0} дена", "{0} дена"], + "days": {"singular": "{0} ден", "dual": "{0} дена", "plural": "{0} дена"}, "week": "една недела", - "weeks": ["{0} недела", "{0} недели", "{0} недели"], + "weeks": { + "singular": "{0} недела", + "dual": "{0} недели", + "plural": "{0} недели", + }, "month": "еден месец", - "months": ["{0} месец", "{0} месеци", "{0} месеци"], + "months": { + "singular": "{0} месец", + "dual": "{0} месеци", + "plural": "{0} месеци", + }, "year": "една година", - "years": ["{0} година", "{0} години", "{0} години"], + "years": { + "singular": "{0} година", + "dual": "{0} години", + "plural": "{0} години", + }, } meridians = {"am": "дп", "pm": "пп", "AM": "претпладне", "PM": "попладне"} From 2ade710d193b54663e4b3b7b85d2125d050be015 Mon Sep 17 00:00:00 2001 From: Kris Fremen Date: Mon, 2 Aug 2021 03:22:45 -0400 Subject: [PATCH 541/649] Add Tests For ChineseCN locale. (#1008) --- tests/test_locales.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_locales.py b/tests/test_locales.py index f25ba9a2e..3e93683e7 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1500,6 +1500,26 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("years", 12) == "12年" +@pytest.mark.usefixtures("lang_locale") +class TestChineseCNLocale: + def test_format_timeframe(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) == "1分钟" + assert self.locale._format_timeframe("minutes", 40) == "40分钟" + assert self.locale._format_timeframe("hour", 1) == "1小时" + assert self.locale._format_timeframe("hours", 23) == "23小时" + assert self.locale._format_timeframe("day", 1) == "1天" + assert self.locale._format_timeframe("days", 12) == "12天" + assert self.locale._format_timeframe("week", 1) == "一周" + assert self.locale._format_timeframe("weeks", 38) == "38周" + assert self.locale._format_timeframe("month", 1) == "1个月" + assert self.locale._format_timeframe("months", 11) == "11个月" + assert self.locale._format_timeframe("year", 1) == "1年" + assert self.locale._format_timeframe("years", 12) == "12年" + + @pytest.mark.usefixtures("lang_locale") class TestSwahiliLocale: def test_format_timeframe(self): From 2ca08e60427392b2eb6a2dd1d72aa37420da7be2 Mon Sep 17 00:00:00 2001 From: Kris Fremen Date: Wed, 11 Aug 2021 22:42:21 -0400 Subject: [PATCH 542/649] Update .pre-commit-config.yaml to newest revisions of hooks. (#1020) Co-authored-by: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ac22f4c2c..1eac547cb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,11 +16,11 @@ repos: - id: check-merge-conflict - id: debug-statements - repo: https://github.com/timothycrosley/isort - rev: 5.9.2 + rev: 5.9.3 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.21.0 + rev: v2.23.3 hooks: - id: pyupgrade args: [--py36-plus] @@ -34,7 +34,7 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/psf/black - rev: 21.6b0 + rev: 21.7b0 hooks: - id: black args: [--safe, --quiet, --target-version=py36] From df74acdbd4c36a0b1bf7d5d0cbba8c87a826194c Mon Sep 17 00:00:00 2001 From: cyriaka90 Date: Wed, 18 Aug 2021 08:37:15 +0200 Subject: [PATCH 543/649] Add Ukrainian Locale Name Variant (#1025) --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index 5a2eaa366..e7871d75a 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1714,7 +1714,7 @@ class BulgarianLocale(SlavicBaseLocale): class UkrainianLocale(SlavicBaseLocale): - names = ["ua", "uk-ua"] + names = ["ua", "uk", "uk-ua"] past = "{0} тому" future = "за {0}" From b1fdaf9ce9b779e636f805d46ef5014132f0a863 Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Wed, 18 Aug 2021 00:06:37 -0700 Subject: [PATCH 544/649] Added Units to Nepali Years Timeframe (#1024) --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index e7871d75a..902b1eed7 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -4254,7 +4254,7 @@ class NepaliLocale(Locale): "month": "एक महिना", "months": "{0} महिना", "year": "एक बर्ष", - "years": "बर्ष", + "years": "{0} बर्ष", } meridians = {"am": "पूर्वाह्न", "pm": "अपरान्ह", "AM": "पूर्वाह्न", "PM": "अपरान्ह"} From de6062c3992d7a9789dfe631aaa0bbf7e80848ac Mon Sep 17 00:00:00 2001 From: NestorTejero Date: Fri, 20 Aug 2021 07:45:57 +0200 Subject: [PATCH 545/649] 1 for numbers is written as 'un' (#1027) --- arrow/locales.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index 902b1eed7..67145b3c7 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -3505,7 +3505,7 @@ class CatalanLocale(Locale): "now": "Ara mateix", "second": "un segon", "seconds": "{0} segons", - "minute": "1 minut", + "minute": "un minut", "minutes": "{0} minuts", "hour": "una hora", "hours": "{0} hores", From e9f5f1a279aaa32e6afad03b2f174a8c2da91595 Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Mon, 23 Aug 2021 02:47:20 -0700 Subject: [PATCH 546/649] Refactor of Icelandic Locale For Dehumanize Support (#1017) * Refactored Icelandic Locale For Humanize Support * Improved Test Coverage * Cleaned up Style * Fixed test case issue * Removed unnecessary test case * Codecov patch fix * Added Error Message Co-authored-by: Anish Nyayachavadi --- arrow/locales.py | 45 +++++++++++++++++++++++++------------------ tests/test_locales.py | 33 ++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 67145b3c7..615217ce0 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2755,33 +2755,40 @@ class MoroccoArabicLocale(ArabicLocale): class IcelandicLocale(Locale): def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: form = self.timeframes[timeframe] - if delta < 0: - form = form[0] - elif delta > 0: - form = form[1] - # FIXME: handle when delta is 0 - return form.format(abs(delta)) # type: ignore + if isinstance(form, Mapping): + if delta < 0: + form = form["past"] + elif delta > 0: + form = form["future"] + else: + raise ValueError( + "Icelandic Locale does not support units with a delta of zero. " + "Please consider making a contribution to fix this issue." + ) + # FIXME: handle when delta is 0 + + return form.format(abs(delta)) names = ["is", "is-is"] past = "fyrir {0} síðan" future = "eftir {0}" - timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[Tuple[str, str], str]]] = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "rétt í þessu", - "second": ("sekúndu", "sekúndu"), - "seconds": ("{0} nokkrum sekúndum", "nokkrar sekúndur"), - "minute": ("einni mínútu", "eina mínútu"), - "minutes": ("{0} mínútum", "{0} mínútur"), - "hour": ("einum tíma", "einn tíma"), - "hours": ("{0} tímum", "{0} tíma"), - "day": ("einum degi", "einn dag"), - "days": ("{0} dögum", "{0} daga"), - "month": ("einum mánuði", "einn mánuð"), - "months": ("{0} mánuðum", "{0} mánuði"), - "year": ("einu ári", "eitt ár"), - "years": ("{0} árum", "{0} ár"), + "second": {"past": "sekúndu", "future": "sekúndu"}, + "seconds": {"past": "{0} nokkrum sekúndum", "future": "nokkrar sekúndur"}, + "minute": {"past": "einni mínútu", "future": "eina mínútu"}, + "minutes": {"past": "{0} mínútum", "future": "{0} mínútur"}, + "hour": {"past": "einum tíma", "future": "einn tíma"}, + "hours": {"past": "{0} tímum", "future": "{0} tíma"}, + "day": {"past": "einum degi", "future": "einn dag"}, + "days": {"past": "{0} dögum", "future": "{0} daga"}, + "month": {"past": "einum mánuði", "future": "einn mánuð"}, + "months": {"past": "{0} mánuðum", "future": "{0} mánuði"}, + "year": {"past": "einu ári", "future": "eitt ár"}, + "years": {"past": "{0} árum", "future": "{0} ár"}, } meridians = {"am": "f.h.", "pm": "e.h.", "AM": "f.h.", "PM": "e.h."} diff --git a/tests/test_locales.py b/tests/test_locales.py index 3e93683e7..bc02df3ae 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -330,12 +330,43 @@ def test_plurals(self): 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" + assert self.locale._format_timeframe("second", 1) == "sekúndu" + assert self.locale._format_timeframe("minute", -1) == "einni mínútu" assert self.locale._format_timeframe("minute", 1) == "eina mínútu" + assert self.locale._format_timeframe("minutes", -2) == "2 mínútum" + assert self.locale._format_timeframe("minutes", 2) == "2 mínútur" + + assert self.locale._format_timeframe("hour", -1) == "einum tíma" + assert self.locale._format_timeframe("hour", 1) == "einn tíma" + assert self.locale._format_timeframe("hours", -2) == "2 tímum" assert self.locale._format_timeframe("hours", 2) == "2 tíma" - assert self.locale._format_timeframe("now", 0) == "rétt í þessu" + + assert self.locale._format_timeframe("day", -1) == "einum degi" + assert self.locale._format_timeframe("day", 1) == "einn dag" + + assert self.locale._format_timeframe("days", -2) == "2 dögum" + assert self.locale._format_timeframe("days", 2) == "2 daga" + + assert self.locale._format_timeframe("month", -1) == "einum mánuði" + assert self.locale._format_timeframe("month", 1) == "einn mánuð" + + assert self.locale._format_timeframe("months", -2) == "2 mánuðum" + assert self.locale._format_timeframe("months", 2) == "2 mánuði" + + assert self.locale._format_timeframe("year", -1) == "einu ári" + assert self.locale._format_timeframe("year", 1) == "eitt ár" + + assert self.locale._format_timeframe("years", -2) == "2 árum" + assert self.locale._format_timeframe("years", 2) == "2 ár" + + with pytest.raises(ValueError): + self.locale._format_timeframe("years", 0) @pytest.mark.usefixtures("lang_locale") From 44c3930d5c6dccff742a82fed9995602e0e3fc87 Mon Sep 17 00:00:00 2001 From: cyriaka90 Date: Fri, 27 Aug 2021 02:22:01 +0200 Subject: [PATCH 547/649] Add Albanian Locale (#1030) --- arrow/constants.py | 2 ++ arrow/locales.py | 81 +++++++++++++++++++++++++++++++++++++++++++ tests/test_arrow.py | 2 ++ tests/test_locales.py | 30 ++++++++++++++++ 4 files changed, 115 insertions(+) diff --git a/arrow/constants.py b/arrow/constants.py index 53be6e8c5..3fb00c124 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -130,6 +130,8 @@ "or-in", "lb", "lb-lu", + "sq", + "sq-al", "ta", "ta-in", "ta-lk", diff --git a/arrow/locales.py b/arrow/locales.py index 615217ce0..e006fe750 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -5528,3 +5528,84 @@ def _ordinal_number(self, n: int) -> str: return f"{n}ஆம்" else: return "" + + +class AlbanianLocale(Locale): + + names = ["sq", "sq-al"] + + past = "{0} më parë" + future = "në {0}" + and_word = "dhe" + + timeframes = { + "now": "tani", + "second": "sekondë", + "seconds": "{0} sekonda", + "minute": "minutë", + "minutes": "{0} minuta", + "hour": "orë", + "hours": "{0} orë", + "day": "ditë", + "days": "{0} ditë", + "week": "javë", + "weeks": "{0} javë", + "month": "muaj", + "months": "{0} muaj", + "year": "vit", + "years": "{0} vjet", + } + + month_names = [ + "", + "janar", + "shkurt", + "mars", + "prill", + "maj", + "qershor", + "korrik", + "gusht", + "shtator", + "tetor", + "nëntor", + "dhjetor", + ] + + month_abbreviations = [ + "", + "jan", + "shk", + "mar", + "pri", + "maj", + "qer", + "korr", + "gush", + "sht", + "tet", + "nën", + "dhj", + ] + + day_names = [ + "", + "e hënë", + "e martë", + "e mërkurë", + "e enjte", + "e premte", + "e shtunë", + "e diel", + ] + + day_abbreviations = [ + "", + "hën", + "mar", + "mër", + "enj", + "pre", + "sht", + "die", + ] diff --git a/tests/test_arrow.py b/tests/test_arrow.py index cef9ee6db..096b97017 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2407,6 +2407,8 @@ def locale_list_no_weeks(): "se-se", "lb", "lb-lu", + "sq", + "sq-al", "ta", "ta-in", "ta-lk", diff --git a/tests/test_locales.py b/tests/test_locales.py index bc02df3ae..072f2ce16 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1383,6 +1383,36 @@ def test_weekday(self): assert self.locale.day_name(dt.isoweekday()) == "uMgqibelo" +@pytest.mark.usefixtures("lang_locale") +class TestAlbanianLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "tani" + assert self.locale._format_timeframe("second", -1) == "sekondë" + assert self.locale._format_timeframe("second", 1) == "sekondë" + assert self.locale._format_timeframe("seconds", -3) == "3 sekonda" + assert self.locale._format_timeframe("minute", 1) == "minutë" + assert self.locale._format_timeframe("minutes", -4) == "4 minuta" + assert self.locale._format_timeframe("hour", 1) == "orë" + assert self.locale._format_timeframe("hours", -23) == "23 orë" + assert self.locale._format_timeframe("day", 1) == "ditë" + assert self.locale._format_timeframe("days", -12) == "12 ditë" + assert self.locale._format_timeframe("week", 1) == "javë" + assert self.locale._format_timeframe("weeks", -12) == "12 javë" + assert self.locale._format_timeframe("month", 1) == "muaj" + assert self.locale._format_timeframe("months", -2) == "2 muaj" + assert self.locale._format_timeframe("year", 1) == "vit" + assert self.locale._format_timeframe("years", -2) == "2 vjet" + + def test_weekday_and_month(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + # Saturday + assert self.locale.day_name(dt.isoweekday()) == "e shtunë" + assert self.locale.day_abbreviation(dt.isoweekday()) == "sht" + # June + assert self.locale.month_name(dt.isoweekday()) == "qershor" + assert self.locale.month_abbreviation(dt.isoweekday()) == "qer" + + @pytest.mark.usefixtures("lang_locale") class TestEstonianLocale: def test_format_timeframe(self): From 6b9e427398846e0daba32c4226ea36c156331591 Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Sun, 29 Aug 2021 11:33:28 -0700 Subject: [PATCH 548/649] Hebrew Locale Refactor (#1029) --- arrow/locales.py | 46 ++++++++++++++++++------------------------- tests/test_locales.py | 18 ++++++++--------- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index e006fe750..3cfa8acd7 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -37,11 +37,6 @@ "months", "year", "years", - "2-hours", - "2-days", - "2-weeks", - "2-months", - "2-years", ] _TimeFrameElements = Union[ @@ -3325,27 +3320,22 @@ class HebrewLocale(Locale): future = "בעוד {0}" and_word = "ו" - timeframes = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "הרגע", "second": "שנייה", "seconds": "{0} שניות", "minute": "דקה", "minutes": "{0} דקות", "hour": "שעה", - "hours": "{0} שעות", - "2-hours": "שעתיים", + "hours": {"2": "שעתיים", "general": "{0} שעות"}, "day": "יום", - "days": "{0} ימים", - "2-days": "יומיים", + "days": {"2": "יומיים", "general": "{0} ימים"}, "week": "שבוע", - "weeks": "{0} שבועות", - "2-weeks": "שבועיים", + "weeks": {"2": "שבועיים", "general": "{0} שבועות"}, "month": "חודש", - "months": "{0} חודשים", - "2-months": "חודשיים", + "months": {"2": "חודשיים", "general": "{0} חודשים"}, "year": "שנה", - "years": "{0} שנים", - "2-years": "שנתיים", + "years": {"2": "שנתיים", "general": "{0} שנים"}, } meridians = { @@ -3389,18 +3379,20 @@ class HebrewLocale(Locale): day_names = ["", "שני", "שלישי", "רביעי", "חמישי", "שישי", "שבת", "ראשון"] day_abbreviations = ["", "ב׳", "ג׳", "ד׳", "ה׳", "ו׳", "ש׳", "א׳"] - def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + def _format_timeframe( + self, timeframe: TimeFrameLiteral, delta: Union[float, int] + ) -> str: """Hebrew couple of aware""" - couple = f"2-{timeframe}" - single = timeframe.rstrip("s") - if abs(delta) == 2 and couple in self.timeframes: - key = couple - elif abs(delta) == 1 and single in self.timeframes: - key = single - else: - key = timeframe + form = self.timeframes[timeframe] + delta = abs(trunc(delta)) - return self.timeframes[key].format(abs(delta)) + if isinstance(form, Mapping): + if delta == 2: + form = form["2"] + else: + form = form["general"] + + return form.format(delta) def describe_multi( self, @@ -3428,7 +3420,7 @@ def describe_multi( humanized += ", " + last_humanized if not only_distance: - humanized = self._format_relative(humanized, timeframe, delta) + humanized = self._format_relative(humanized, timeframe, trunc(delta)) return humanized diff --git a/tests/test_locales.py b/tests/test_locales.py index 072f2ce16..88d4bff61 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -723,11 +723,11 @@ def test_multi_describe_mk(self): @pytest.mark.usefixtures("lang_locale") class TestHebrewLocale: def test_couple_of_timeframe(self): - assert self.locale._format_timeframe("days", 1) == "יום" + assert self.locale._format_timeframe("day", 1) == "יום" assert self.locale._format_timeframe("days", 2) == "יומיים" assert self.locale._format_timeframe("days", 3) == "3 ימים" - assert self.locale._format_timeframe("hours", 1) == "שעה" + assert self.locale._format_timeframe("hour", 1) == "שעה" assert self.locale._format_timeframe("hours", 2) == "שעתיים" assert self.locale._format_timeframe("hours", 3) == "3 שעות" @@ -735,31 +735,31 @@ def test_couple_of_timeframe(self): assert self.locale._format_timeframe("weeks", 2) == "שבועיים" assert self.locale._format_timeframe("weeks", 3) == "3 שבועות" - assert self.locale._format_timeframe("months", 1) == "חודש" + assert self.locale._format_timeframe("month", 1) == "חודש" assert self.locale._format_timeframe("months", 2) == "חודשיים" assert self.locale._format_timeframe("months", 4) == "4 חודשים" - assert self.locale._format_timeframe("years", 1) == "שנה" + assert self.locale._format_timeframe("year", 1) == "שנה" assert self.locale._format_timeframe("years", 2) == "שנתיים" assert self.locale._format_timeframe("years", 5) == "5 שנים" def test_describe_multi(self): describe = self.locale.describe_multi - fulltest = [("years", 5), ("weeks", 1), ("hours", 1), ("minutes", 6)] + fulltest = [("years", 5), ("week", 1), ("hour", 1), ("minutes", 6)] assert describe(fulltest) == "בעוד 5 שנים, שבוע, שעה ו־6 דקות" - seconds4000_0days = [("days", 0), ("hours", 1), ("minutes", 6)] + seconds4000_0days = [("days", 0), ("hour", 1), ("minutes", 6)] assert describe(seconds4000_0days) == "בעוד 0 ימים, שעה ו־6 דקות" - seconds4000 = [("hours", 1), ("minutes", 6)] + seconds4000 = [("hour", 1), ("minutes", 6)] assert describe(seconds4000) == "בעוד שעה ו־6 דקות" assert describe(seconds4000, only_distance=True) == "שעה ו־6 דקות" - seconds3700 = [("hours", 1), ("minutes", 1)] + seconds3700 = [("hour", 1), ("minute", 1)] assert describe(seconds3700) == "בעוד שעה ודקה" seconds300_0hours = [("hours", 0), ("minutes", 5)] assert describe(seconds300_0hours) == "בעוד 0 שעות ו־5 דקות" seconds300 = [("minutes", 5)] assert describe(seconds300) == "בעוד 5 דקות" - seconds60 = [("minutes", 1)] + seconds60 = [("minute", 1)] assert describe(seconds60) == "בעוד דקה" assert describe(seconds60, only_distance=True) == "דקה" From 906802c255fd59ee7c1ebe6dabf786658b0e2cf1 Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Sun, 29 Aug 2021 12:42:23 -0700 Subject: [PATCH 549/649] Refactored Arabic Locale for Dehumanize Support (#1028) --- arrow/locales.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 3cfa8acd7..75c7a1695 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2541,17 +2541,17 @@ class ArabicLocale(Locale): timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "الآن", "second": "ثانية", - "seconds": {"double": "ثانيتين", "ten": "{0} ثوان", "higher": "{0} ثانية"}, + "seconds": {"2": "ثانيتين", "ten": "{0} ثوان", "higher": "{0} ثانية"}, "minute": "دقيقة", - "minutes": {"double": "دقيقتين", "ten": "{0} دقائق", "higher": "{0} دقيقة"}, + "minutes": {"2": "دقيقتين", "ten": "{0} دقائق", "higher": "{0} دقيقة"}, "hour": "ساعة", - "hours": {"double": "ساعتين", "ten": "{0} ساعات", "higher": "{0} ساعة"}, + "hours": {"2": "ساعتين", "ten": "{0} ساعات", "higher": "{0} ساعة"}, "day": "يوم", - "days": {"double": "يومين", "ten": "{0} أيام", "higher": "{0} يوم"}, + "days": {"2": "يومين", "ten": "{0} أيام", "higher": "{0} يوم"}, "month": "شهر", - "months": {"double": "شهرين", "ten": "{0} أشهر", "higher": "{0} شهر"}, + "months": {"2": "شهرين", "ten": "{0} أشهر", "higher": "{0} شهر"}, "year": "سنة", - "years": {"double": "سنتين", "ten": "{0} سنوات", "higher": "{0} سنة"}, + "years": {"2": "سنتين", "ten": "{0} سنوات", "higher": "{0} سنة"}, } month_names = [ @@ -2602,7 +2602,7 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: delta = abs(delta) if isinstance(form, Mapping): if delta == 2: - form = form["double"] + form = form["2"] elif 2 < delta <= 10: form = form["ten"] else: From 72301727ab3e9e9ab018dfe87ed1362641b126df Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Sun, 29 Aug 2021 13:35:43 -0700 Subject: [PATCH 550/649] Refactor of Czech Locale For Dehumanize Support (#1021) --- arrow/locales.py | 64 ++++++++++++++++++++++++++++++------------- tests/test_locales.py | 2 ++ 2 files changed, 47 insertions(+), 19 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 75c7a1695..2e351fc54 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -3046,24 +3046,50 @@ class HindiLocale(Locale): class CzechLocale(Locale): names = ["cs", "cs-cz"] - timeframes: ClassVar[ - Mapping[TimeFrameLiteral, Union[Mapping[str, Union[List[str], str]], str]] - ] = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "Teď", "second": {"past": "vteřina", "future": "vteřina", "zero": "vteřina"}, - "seconds": {"past": "{0} sekundami", "future": ["{0} sekundy", "{0} sekund"]}, + "seconds": { + "past": "{0} sekundami", + "future-singular": "{0} sekundy", + "future-paucal": "{0} sekund", + }, "minute": {"past": "minutou", "future": "minutu", "zero": "{0} minut"}, - "minutes": {"past": "{0} minutami", "future": ["{0} minuty", "{0} minut"]}, + "minutes": { + "past": "{0} minutami", + "future-singular": "{0} minuty", + "future-paucal": "{0} minut", + }, "hour": {"past": "hodinou", "future": "hodinu", "zero": "{0} hodin"}, - "hours": {"past": "{0} hodinami", "future": ["{0} hodiny", "{0} hodin"]}, + "hours": { + "past": "{0} hodinami", + "future-singular": "{0} hodiny", + "future-paucal": "{0} hodin", + }, "day": {"past": "dnem", "future": "den", "zero": "{0} dnů"}, - "days": {"past": "{0} dny", "future": ["{0} dny", "{0} dnů"]}, + "days": { + "past": "{0} dny", + "future-singular": "{0} dny", + "future-paucal": "{0} dnů", + }, "week": {"past": "týdnem", "future": "týden", "zero": "{0} týdnů"}, - "weeks": {"past": "{0} týdny", "future": ["{0} týdny", "{0} týdnů"]}, + "weeks": { + "past": "{0} týdny", + "future-singular": "{0} týdny", + "future-paucal": "{0} týdnů", + }, "month": {"past": "měsícem", "future": "měsíc", "zero": "{0} měsíců"}, - "months": {"past": "{0} měsíci", "future": ["{0} měsíce", "{0} měsíců"]}, + "months": { + "past": "{0} měsíci", + "future-singular": "{0} měsíce", + "future-paucal": "{0} měsíců", + }, "year": {"past": "rokem", "future": "rok", "zero": "{0} let"}, - "years": {"past": "{0} lety", "future": ["{0} roky", "{0} let"]}, + "years": { + "past": "{0} lety", + "future-singular": "{0} roky", + "future-paucal": "{0} let", + }, } past = "Před {0}" @@ -3123,20 +3149,20 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: if delta == 0: key = "zero" # And *never* use 0 in the singular! - elif delta > 0: - key = "future" - else: + elif delta < 0: key = "past" - form: Union[List[str], str] = form[key] - - if isinstance(form, list): - if 2 <= abs_delta % 10 <= 4 and ( + else: + # Needed since both regular future and future-singular and future-paucal cases + if "future-singular" not in form: + key = "future" + elif 2 <= abs_delta % 10 <= 4 and ( abs_delta % 100 < 10 or abs_delta % 100 >= 20 ): - form = form[0] + key = "future-singular" else: - form = form[1] + key = "future-paucal" + form: str = form[key] return form.format(abs_delta) diff --git a/tests/test_locales.py b/tests/test_locales.py index 88d4bff61..34f855cb4 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -452,6 +452,8 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("hour", 0) == "0 hodin" assert self.locale._format_timeframe("hours", -2) == "2 hodinami" assert self.locale._format_timeframe("hours", -5) == "5 hodinami" + assert self.locale._format_timeframe("hour", 1) == "hodinu" + assert self.locale._format_timeframe("now", 0) == "Teď" assert self.locale._format_timeframe("weeks", 2) == "2 týdny" From c3149a7327c4f859fd7876907f11662c544cd091 Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Thu, 2 Sep 2021 11:04:43 -0700 Subject: [PATCH 551/649] Slovak Locale Refactor (#1023) --- arrow/locales.py | 63 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 2e351fc54..617fb6ceb 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -3169,24 +3169,50 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: class SlovakLocale(Locale): names = ["sk", "sk-sk"] - timeframes: ClassVar[ - Mapping[TimeFrameLiteral, Union[Mapping[str, Union[List[str], str]], str]] - ] = { + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "Teraz", "second": {"past": "sekundou", "future": "sekundu", "zero": "{0} sekúnd"}, - "seconds": {"past": "{0} sekundami", "future": ["{0} sekundy", "{0} sekúnd"]}, + "seconds": { + "past": "{0} sekundami", + "future-singular": "{0} sekundy", + "future-paucal": "{0} sekúnd", + }, "minute": {"past": "minútou", "future": "minútu", "zero": "{0} minút"}, - "minutes": {"past": "{0} minútami", "future": ["{0} minúty", "{0} minút"]}, + "minutes": { + "past": "{0} minútami", + "future-singular": "{0} minúty", + "future-paucal": "{0} minút", + }, "hour": {"past": "hodinou", "future": "hodinu", "zero": "{0} hodín"}, - "hours": {"past": "{0} hodinami", "future": ["{0} hodiny", "{0} hodín"]}, + "hours": { + "past": "{0} hodinami", + "future-singular": "{0} hodiny", + "future-paucal": "{0} hodín", + }, "day": {"past": "dňom", "future": "deň", "zero": "{0} dní"}, - "days": {"past": "{0} dňami", "future": ["{0} dni", "{0} dní"]}, + "days": { + "past": "{0} dňami", + "future-singular": "{0} dni", + "future-paucal": "{0} dní", + }, "week": {"past": "týždňom", "future": "týždeň", "zero": "{0} týždňov"}, - "weeks": {"past": "{0} týždňami", "future": ["{0} týždne", "{0} týždňov"]}, + "weeks": { + "past": "{0} týždňami", + "future-singular": "{0} týždne", + "future-paucal": "{0} týždňov", + }, "month": {"past": "mesiacom", "future": "mesiac", "zero": "{0} mesiacov"}, - "months": {"past": "{0} mesiacmi", "future": ["{0} mesiace", "{0} mesiacov"]}, + "months": { + "past": "{0} mesiacmi", + "future-singular": "{0} mesiace", + "future-paucal": "{0} mesiacov", + }, "year": {"past": "rokom", "future": "rok", "zero": "{0} rokov"}, - "years": {"past": "{0} rokmi", "future": ["{0} roky", "{0} rokov"]}, + "years": { + "past": "{0} rokmi", + "future-singular": "{0} roky", + "future-paucal": "{0} rokov", + }, } past = "Pred {0}" @@ -3247,20 +3273,19 @@ def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: if delta == 0: key = "zero" # And *never* use 0 in the singular! - elif delta > 0: - key = "future" - else: + elif delta < 0: key = "past" - form: Union[List[str], str] = form[key] - - if isinstance(form, list): - if 2 <= abs_delta % 10 <= 4 and ( + else: + if "future-singular" not in form: + key = "future" + elif 2 <= abs_delta % 10 <= 4 and ( abs_delta % 100 < 10 or abs_delta % 100 >= 20 ): - form = form[0] + key = "future-singular" else: - form = form[1] + key = "future-paucal" + form: str = form[key] return form.format(abs_delta) From 80e5947de29b49c52c84c7bce9dedbf5532606fa Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Thu, 2 Sep 2021 16:20:28 -0700 Subject: [PATCH 552/649] Refactored Finnish Locale (#1031) --- arrow/locales.py | 54 ++++++++++++++++++------------------------- tests/test_locales.py | 15 ++++++++---- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 617fb6ceb..02161b6bb 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -857,20 +857,20 @@ class FinnishLocale(Locale): past = "{0} sitten" future = "{0} kuluttua" - timeframes: ClassVar[Mapping[TimeFrameLiteral, List[str]]] = { - "now": ["juuri nyt", "juuri nyt"], - "second": ["sekunti", "sekunti"], - "seconds": ["{0} muutama sekunti", "{0} muutaman sekunnin"], - "minute": ["minuutti", "minuutin"], - "minutes": ["{0} minuuttia", "{0} minuutin"], - "hour": ["tunti", "tunnin"], - "hours": ["{0} tuntia", "{0} tunnin"], - "day": ["päivä", "päivä"], - "days": ["{0} päivää", "{0} päivän"], - "month": ["kuukausi", "kuukauden"], - "months": ["{0} kuukautta", "{0} kuukauden"], - "year": ["vuosi", "vuoden"], - "years": ["{0} vuotta", "{0} vuoden"], + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { + "now": "juuri nyt", + "second": "sekunti", + "seconds": {"past": "{0} muutama sekunti", "future": "{0} muutaman 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ä", + "days": {"past": "{0} päivää", "future": "{0} päivän"}, + "month": {"past": "kuukausi", "future": "kuukauden"}, + "months": {"past": "{0} kuukautta", "future": "{0} kuukauden"}, + "year": {"past": "vuosi", "future": "vuoden"}, + "years": {"past": "{0} vuotta", "future": "{0} vuoden"}, } # Months and days are lowercase in Finnish @@ -919,26 +919,16 @@ class FinnishLocale(Locale): day_abbreviations = ["", "ma", "ti", "ke", "to", "pe", "la", "su"] - # TODO: Fix return type - def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> Tuple[str, str]: # type: ignore - return ( - self.timeframes[timeframe][0].format(abs(delta)), - self.timeframes[timeframe][1].format(abs(delta)), - ) - - def _format_relative( - self, - humanized: str, - timeframe: TimeFrameLiteral, - delta: Union[float, int], - ) -> str: - if timeframe == "now": - return humanized[0] + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + form = self.timeframes[timeframe] - direction = self.past if delta < 0 else self.future - which = 0 if delta < 0 else 1 + if isinstance(form, Mapping): + if delta < 0: + form = form["past"] + else: + form = form["future"] - return direction.format(humanized[which]) + return form.format(abs(delta)) def _ordinal_number(self, n: int) -> str: return f"{n}." diff --git a/tests/test_locales.py b/tests/test_locales.py index 34f855cb4..5cb0ab8f4 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -799,19 +799,24 @@ def test_ordinal_number(self): @pytest.mark.usefixtures("lang_locale") class TestFinnishLocale: def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == ("2 tuntia", "2 tunnin") - assert self.locale._format_timeframe("hour", 0) == ("tunti", "tunnin") + assert self.locale._format_timeframe("hours", -2) == "2 tuntia" + assert self.locale._format_timeframe("hours", 2) == "2 tunnin" + + assert self.locale._format_timeframe("hour", -1) == "tunti" + assert self.locale._format_timeframe("hour", 1) == "tunnin" + + assert self.locale._format_timeframe("now", 1) == "juuri nyt" def test_format_relative_now(self): - result = self.locale._format_relative(["juuri nyt", "juuri nyt"], "now", 0) + result = self.locale._format_relative("juuri nyt", "now", 0) assert result == "juuri nyt" def test_format_relative_past(self): - result = self.locale._format_relative(["tunti", "tunnin"], "hour", 1) + result = self.locale._format_relative("tunnin", "hour", 1) assert result == "tunnin kuluttua" def test_format_relative_future(self): - result = self.locale._format_relative(["tunti", "tunnin"], "hour", -1) + result = self.locale._format_relative("tunti", "hour", -1) assert result == "tunti sitten" def test_ordinal_number(self): From 246c55d3aeeeec67ab98f32e4f921072fa1c2029 Mon Sep 17 00:00:00 2001 From: Kris Fremen Date: Sun, 5 Sep 2021 01:21:36 -0400 Subject: [PATCH 553/649] Add Support for Decimal as Input to arrow.get. (#1035) --- arrow/factory.py | 3 +++ tests/test_factory.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/arrow/factory.py b/arrow/factory.py index 5787dcf02..aad4af8bd 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -9,6 +9,7 @@ import calendar from datetime import date, datetime 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 @@ -218,6 +219,8 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: if arg_count == 1: arg = args[0] + if isinstance(arg, Decimal): + arg = float(arg) # (None) -> raises an exception if arg is None: diff --git a/tests/test_factory.py b/tests/test_factory.py index 4bb81e872..53bba20d3 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -1,5 +1,6 @@ import time from datetime import date, datetime +from decimal import Decimal import pytest from dateutil import tz @@ -263,6 +264,13 @@ def test_one_arg_bool(self): with pytest.raises(TypeError): self.factory.get(True) + def test_one_arg_decimal(self): + result = self.factory.get(Decimal(1577836800.26843)) + + assert result._datetime == datetime( + 2020, 1, 1, 0, 0, 0, 268430, tzinfo=tz.tzutc() + ) + def test_two_args_datetime_tzinfo(self): result = self.factory.get(datetime(2013, 1, 1), tz.gettz("US/Pacific")) From 8ad64d0c831d95622c42542e43201f61c0d8c4db Mon Sep 17 00:00:00 2001 From: Kris Fremen Date: Sun, 5 Sep 2021 16:02:56 -0400 Subject: [PATCH 554/649] Default to UTC for arrow.fromtimestamp() (#1036) --- arrow/arrow.py | 4 ++-- tests/test_arrow.py | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index d01fa8943..874f96e0f 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -246,12 +246,12 @@ def fromtimestamp( the given timezone. :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. - :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. + :param tzinfo: (optional) a ``tzinfo`` object. Defaults to UTC time. """ if tzinfo is None: - tzinfo = dateutil_tz.tzlocal() + tzinfo = dateutil_tz.tzutc() elif isinstance(tzinfo, str): tzinfo = parser.TzinfoParser.parse(tzinfo) diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 096b97017..cd963ab0d 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -102,11 +102,10 @@ def test_utcnow(self): def test_fromtimestamp(self): - timestamp = time.time() - - result = arrow.Arrow.fromtimestamp(timestamp) + timestamp = datetime.utcnow().timestamp() + result = arrow.Arrow.fromtimestamp(timestamp, tzinfo=tz.tzutc()) assert_datetime_equality( - result._datetime, datetime.now().replace(tzinfo=tz.tzlocal()) + result._datetime, datetime.fromtimestamp(timestamp, tz=tz.tzutc()) ) result = arrow.Arrow.fromtimestamp(timestamp, tzinfo=tz.gettz("Europe/Paris")) From 4c13420f4fad95697047b56700817b2d154393da Mon Sep 17 00:00:00 2001 From: Kris Fremen Date: Sun, 5 Sep 2021 20:54:14 -0400 Subject: [PATCH 555/649] Revert "Default to UTC for arrow.fromtimestamp() (#1036)" (#1037) This reverts commit 8ad64d0c831d95622c42542e43201f61c0d8c4db. --- arrow/arrow.py | 4 ++-- tests/test_arrow.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 874f96e0f..d01fa8943 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -246,12 +246,12 @@ def fromtimestamp( the given timezone. :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. - :param tzinfo: (optional) a ``tzinfo`` object. Defaults to UTC time. + :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. """ if tzinfo is None: - tzinfo = dateutil_tz.tzutc() + tzinfo = dateutil_tz.tzlocal() elif isinstance(tzinfo, str): tzinfo = parser.TzinfoParser.parse(tzinfo) diff --git a/tests/test_arrow.py b/tests/test_arrow.py index cd963ab0d..096b97017 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -102,10 +102,11 @@ def test_utcnow(self): def test_fromtimestamp(self): - timestamp = datetime.utcnow().timestamp() - result = arrow.Arrow.fromtimestamp(timestamp, tzinfo=tz.tzutc()) + timestamp = time.time() + + result = arrow.Arrow.fromtimestamp(timestamp) assert_datetime_equality( - result._datetime, datetime.fromtimestamp(timestamp, tz=tz.tzutc()) + result._datetime, datetime.now().replace(tzinfo=tz.tzlocal()) ) result = arrow.Arrow.fromtimestamp(timestamp, tzinfo=tz.gettz("Europe/Paris")) From 4eda42bbae7f3fef3fbd61a9c0ced89ccf6aa6bc Mon Sep 17 00:00:00 2001 From: Charalampos Poulikidis Date: Fri, 10 Sep 2021 21:14:29 +0300 Subject: [PATCH 556/649] Add "week" timeframe locale for greek language (#1040) --- arrow/locales.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 02161b6bb..6221df7ad 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -666,6 +666,8 @@ class GreekLocale(Locale): "hours": "{0} ώρες", "day": "μία μέρα", "days": "{0} μέρες", + "week": "μία εβδομάδα", + "weeks": "{0} εβδομάδες", "month": "ένα μήνα", "months": "{0} μήνες", "year": "ένα χρόνο", From 310cef824eb9a321388079242adce6b8f31d0b0a Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Sat, 11 Sep 2021 17:12:25 -0400 Subject: [PATCH 557/649] Added new Locales that Dehumanize Supports (#1034) Co-authored-by: Chris <30196510+systemcatch@users.noreply.github.com> --- arrow/arrow.py | 77 ++++++++++++++++++++++++++------------------- arrow/constants.py | 8 +++++ tests/test_arrow.py | 20 +++++++++--- 3 files changed, 68 insertions(+), 37 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index d01fa8943..ad95cacdf 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1299,7 +1299,7 @@ def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float: "Please consider making a contribution to this locale." ) - def dehumanize(self, timestring: str, locale: str = "en_us") -> "Arrow": + 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 :class:`Arrow ` object. @@ -1352,43 +1352,56 @@ def dehumanize(self, timestring: str, locale: str = "en_us") -> "Arrow": # Create a regex pattern object for numbers num_pattern = re.compile(r"\d+") - # Search timestring for each time unit within locale - for unit in locale_obj.timeframes: + # Search input string for each time unit within locale + for unit, unit_object in locale_obj.timeframes.items(): - # Numeric unit of change - change_value = 0 + # Need to check the type of unit_object to create the correct dictionary + if isinstance(unit_object, Mapping): + strings_to_search = unit_object + else: + strings_to_search = {unit: str(unit_object)} - # Replace {0} with regex \d representing digits - search_string = str(locale_obj.timeframes[unit]) - search_string = search_string.format(r"\d+") + # Search for any matches that exist for that locale's unit. + # 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(): - # Create search pattern and find within string - pattern = re.compile(fr"{search_string}") - match = pattern.search(timestring) + # Replace {0} with regex \d representing digits + search_string = str(time_string) + search_string = search_string.format(r"\d+") - # If there is no match continue to next iteration - if not match: - continue + # Create search pattern and find within string + pattern = re.compile(fr"{search_string}") + match = pattern.search(input_string) - match_string = match.group() - num_match = num_pattern.search(match_string) + # If there is no match continue to next iteration + if not match: + continue - # If no number matches set change value to be one - if not num_match: - change_value = 1 - else: - change_value = int(num_match.group()) + match_string = match.group() + num_match = num_pattern.search(match_string) + + # If no number matches + # Need for absolute value as some locales have signs included in their objects + if not num_match: + change_value = ( + 1 if not time_delta.isnumeric() else abs(int(time_delta)) + ) + else: + change_value = int(num_match.group()) - # No time to update if now is the unit - if unit == "now": - unit_visited[unit] = True - continue + # No time to update if now is the unit + if unit == "now": + unit_visited[unit] = True + continue - # Add change value to the correct unit (incorporates the plurality that exists within timeframe i.e second v.s seconds) - time_unit_to_change = str(unit) - time_unit_to_change += "s" if (str(time_unit_to_change)[-1] != "s") else "" - time_object_info[time_unit_to_change] = change_value - unit_visited[time_unit_to_change] = True + # Add change value to the correct unit (incorporates the plurality that exists within timeframe i.e second v.s seconds) + time_unit_to_change = str(unit) + time_unit_to_change += ( + "s" if (str(time_unit_to_change)[-1] != "s") else "" + ) + time_object_info[time_unit_to_change] = change_value + unit_visited[time_unit_to_change] = True # Assert error if string does not modify any units if not any([True for k, v in unit_visited.items() if v]): @@ -1401,12 +1414,12 @@ def dehumanize(self, timestring: str, locale: str = "en_us") -> "Arrow": future_string = locale_obj.future future_string = future_string.format(".*") future_pattern = re.compile(fr"^{future_string}$") - future_pattern_match = future_pattern.findall(timestring) + 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_match = past_pattern.findall(timestring) + 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 # was visited before raising a ValueError diff --git a/arrow/constants.py b/arrow/constants.py index 3fb00c124..085ec392f 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -65,6 +65,8 @@ "se-se", "sv", "sv-se", + "fi", + "fi-fi", "zh", "zh-cn", "zh-tw", @@ -116,6 +118,10 @@ "sl-si", "id", "id-id", + "ne", + "ne-np", + "ee", + "et", "sw", "sw-ke", "sw-tz", @@ -130,6 +136,8 @@ "or-in", "lb", "lb-lu", + "zu", + "zu-za", "sq", "sq-al", "ta", diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 096b97017..f217909a8 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2338,6 +2338,8 @@ def locale_list_no_weeks(): "ja-jp", "sv", "sv-se", + "fi", + "fi-fi", "zh", "zh-cn", "zh-tw", @@ -2389,6 +2391,10 @@ def locale_list_no_weeks(): "sl-si", "id", "id-id", + "ne", + "ne-np", + "ee", + "et", "sw", "sw-ke", "sw-tz", @@ -2407,6 +2413,8 @@ def locale_list_no_weeks(): "se-se", "lb", "lb-lu", + "zu", + "zu-za", "sq", "sq-al", "ta", @@ -2469,6 +2477,8 @@ def locale_list_with_weeks(): "ms-bn", "lb", "lb-lu", + "zu", + "zu-za", "ta", "ta-in", "ta-lk", @@ -2726,18 +2736,18 @@ def test_unsupported_locale(self): second_future = arw.shift(seconds=5) second_ago_string = second_ago.humanize( - arw, locale="fi", granularity=["second"] + arw, locale="ko", granularity=["second"] ) second_future_string = second_future.humanize( - arw, locale="fi", granularity=["second"] + arw, locale="ko", granularity=["second"] ) - # fi is an example of many unsupported locales currently + # ko is an example of many unsupported locales currently with pytest.raises(ValueError): - arw.dehumanize(second_ago_string, locale="fi") + arw.dehumanize(second_ago_string, locale="ko") with pytest.raises(ValueError): - arw.dehumanize(second_future_string, locale="fi") + arw.dehumanize(second_future_string, locale="ko") # Test to ensure old style locale strings are supported def test_normalized_locale(self): From 3f056f35fd67f0581de8c126882bc2801067efd4 Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Sat, 11 Sep 2021 22:24:51 +0100 Subject: [PATCH 558/649] Bump version to 1.2.0 and update CHANGELOG (#1039) Co-authored-by: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> --- CHANGELOG.rst | 10 ++++++++++ arrow/_version.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5b0fb7df3..abb6cc297 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,16 @@ Changelog ========= +1.2.0 (2021-09-06) +------------------ + +- [NEW] Added Albanian, Tamil and Zulu locales. +- [NEW] Added support for ``Decimal`` as input to ``arrow.get()``. +- [FIX] The Estonian, Finnish, Nepali and Zulu locales now support ``dehumanize``. +- [FIX] Improved validation checks when using parser tokens ``A`` and ``hh``. +- [FIX] Minor bug fixes to Catalan, Cantonese, Greek and Nepali locales. +- [INTERNAL] Updated ``dateutil`` requirement in ``setup.py`` to ``>=2.8.2``. + 1.1.1 (2021-06-24) ------------------ diff --git a/arrow/_version.py b/arrow/_version.py index a82b376d2..c68196d1c 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "1.1.1" +__version__ = "1.2.0" From a4f179721f9fb33785d2b0171dfabd1f643e74ac Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 12 Sep 2021 12:26:45 -0400 Subject: [PATCH 559/649] Revert dateutil lock back to >= 2.7.0 (#1041) --- CHANGELOG.rst | 3 +-- requirements-dev.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index abb6cc297..0294bad7e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -1.2.0 (2021-09-06) +1.2.0 (2021-09-12) ------------------ - [NEW] Added Albanian, Tamil and Zulu locales. @@ -9,7 +9,6 @@ Changelog - [FIX] The Estonian, Finnish, Nepali and Zulu locales now support ``dehumanize``. - [FIX] Improved validation checks when using parser tokens ``A`` and ``hh``. - [FIX] Minor bug fixes to Catalan, Cantonese, Greek and Nepali locales. -- [INTERNAL] Updated ``dateutil`` requirement in ``setup.py`` to ``>=2.8.2``. 1.1.1 (2021-06-24) ------------------ diff --git a/requirements-dev.txt b/requirements-dev.txt index 84acf695b..a04cbef76 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ pre-commit==2.* pytest==6.* pytest-cov==2.* pytest-mock==3.* -python-dateutil>=2.8.2 +python-dateutil>=2.7.0 pytz==2021.* simplejson==3.* sphinx==4.* diff --git a/setup.py b/setup.py index 65b52b7dc..df0a5a759 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ zip_safe=False, python_requires=">=3.6", install_requires=[ - "python-dateutil>=2.8.2", + "python-dateutil>=2.7.0", "typing_extensions; python_version<'3.8'", ], classifiers=[ From 9b457195918a3b69960a88ff3be321b8d5a7c262 Mon Sep 17 00:00:00 2001 From: Guy Date: Tue, 21 Sep 2021 21:51:25 +0300 Subject: [PATCH 560/649] Fixing Plural of Years and Days in Hebrew (#1042) --- arrow/locales.py | 22 ++++++++++------------ tests/test_arrow.py | 16 ++++++++++++++++ tests/test_locales.py | 2 ++ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 6221df7ad..bdfb05677 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -3370,15 +3370,15 @@ class HebrewLocale(Locale): "minute": "דקה", "minutes": "{0} דקות", "hour": "שעה", - "hours": {"2": "שעתיים", "general": "{0} שעות"}, + "hours": {"2": "שעתיים", "ten": "{0} שעות", "higher": "{0} שעות"}, "day": "יום", - "days": {"2": "יומיים", "general": "{0} ימים"}, + "days": {"2": "יומיים", "ten": "{0} ימים", "higher": "{0} יום"}, "week": "שבוע", - "weeks": {"2": "שבועיים", "general": "{0} שבועות"}, + "weeks": {"2": "שבועיים", "ten": "{0} שבועות", "higher": "{0} שבועות"}, "month": "חודש", - "months": {"2": "חודשיים", "general": "{0} חודשים"}, + "months": {"2": "חודשיים", "ten": "{0} חודשים", "higher": "{0} חודשים"}, "year": "שנה", - "years": {"2": "שנתיים", "general": "{0} שנים"}, + "years": {"2": "שנתיים", "ten": "{0} שנים", "higher": "{0} שנה"}, } meridians = { @@ -3422,18 +3422,16 @@ class HebrewLocale(Locale): day_names = ["", "שני", "שלישי", "רביעי", "חמישי", "שישי", "שבת", "ראשון"] day_abbreviations = ["", "ב׳", "ג׳", "ד׳", "ה׳", "ו׳", "ש׳", "א׳"] - def _format_timeframe( - self, timeframe: TimeFrameLiteral, delta: Union[float, int] - ) -> str: - """Hebrew couple of aware""" + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: form = self.timeframes[timeframe] - delta = abs(trunc(delta)) - + delta = abs(delta) if isinstance(form, Mapping): if delta == 2: form = form["2"] + elif delta == 0 or 2 < delta <= 10: + form = form["ten"] else: - form = form["general"] + form = form["higher"] return form.format(delta) diff --git a/tests/test_arrow.py b/tests/test_arrow.py index f217909a8..732cd79c2 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2656,6 +2656,22 @@ def test_years(self, locale_list_no_weeks): assert arw.dehumanize(year_ago_string, locale=lang) == year_ago assert arw.dehumanize(year_future_string, locale=lang) == year_future + def test_gt_than_10_years(self, locale_list_no_weeks): + + 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) + + year_ago_string = year_ago.humanize(arw, locale=lang, granularity=["year"]) + year_future_string = year_future.humanize( + arw, locale=lang, granularity=["year"] + ) + + assert arw.dehumanize(year_ago_string, locale=lang) == year_ago + assert arw.dehumanize(year_future_string, locale=lang) == year_future + def test_mixed_granularity(self, locale_list_no_weeks): for lang in locale_list_no_weeks: diff --git a/tests/test_locales.py b/tests/test_locales.py index 5cb0ab8f4..a69a085ce 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -728,6 +728,7 @@ def test_couple_of_timeframe(self): assert self.locale._format_timeframe("day", 1) == "יום" assert self.locale._format_timeframe("days", 2) == "יומיים" assert self.locale._format_timeframe("days", 3) == "3 ימים" + assert self.locale._format_timeframe("days", 80) == "80 יום" assert self.locale._format_timeframe("hour", 1) == "שעה" assert self.locale._format_timeframe("hours", 2) == "שעתיים" @@ -744,6 +745,7 @@ def test_couple_of_timeframe(self): assert self.locale._format_timeframe("year", 1) == "שנה" assert self.locale._format_timeframe("years", 2) == "שנתיים" assert self.locale._format_timeframe("years", 5) == "5 שנים" + assert self.locale._format_timeframe("years", 15) == "15 שנה" def test_describe_multi(self): describe = self.locale.describe_multi From 2f54cb8edd26cf3f893c859da3f079a299f0f517 Mon Sep 17 00:00:00 2001 From: ChingYi-AX <82449718+ChingYi-AX@users.noreply.github.com> Date: Tue, 28 Sep 2021 17:31:26 +0200 Subject: [PATCH 561/649] Add Sinhala Locale (#1044) --- arrow/locales.py | 164 ++++++++++++++++++++++++++++++++++++++++++ tests/test_locales.py | 74 +++++++++++++++++++ 2 files changed, 238 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index bdfb05677..48413276f 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -5642,3 +5642,167 @@ class AlbanianLocale(Locale): "sht", "die", ] + + +class SinhalaLocale(Locale): + + names = ["si", "si-lk"] + + past = "{0}ට පෙර" + future = "{0}" + and_word = "සහ" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[Mapping[str, str], str]]] = { + "now": "දැන්", + "second": { + "past": "තත්පරයක", + "future": "තත්පරයකින්", + }, # ක් is the article + "seconds": { + "past": "තත්පර {0} ක", + "future": "තත්පර {0} කින්", + }, + "minute": { + "past": "විනාඩියක", + "future": "විනාඩියකින්", + }, + "minutes": { + "past": "විනාඩි {0} ක", + "future": "මිනිත්තු {0} කින්", + }, + "hour": {"past": "පැයක", "future": "පැයකින්"}, + "hours": { + "past": "පැය {0} ක", + "future": "පැය {0} කින්", + }, + "day": {"past": "දිනක", "future": "දිනකට"}, + "days": { + "past": "දින {0} ක", + "future": "දින {0} කින්", + }, + "week": {"past": "සතියක", "future": "සතියකින්"}, + "weeks": { + "past": "සති {0} ක", + "future": "සති {0} කින්", + }, + "month": {"past": "මාසයක", "future": "එය මාසය තුළ"}, + "months": { + "past": "මාස {0} ක", + "future": "මාස {0} කින්", + }, + "year": {"past": "වසරක", "future": "වසරක් තුළ"}, + "years": { + "past": "අවුරුදු {0} ක", + "future": "අවුරුදු {0} තුළ", + }, + } + # Sinhala: the general format to describe timeframe is different from past and future, + # so we do not copy the original timeframes dictionary + timeframes_only_distance = dict() + timeframes_only_distance["second"] = "තත්පරයක්" + timeframes_only_distance["seconds"] = "තත්පර {0}" + timeframes_only_distance["minute"] = "මිනිත්තුවක්" + timeframes_only_distance["minutes"] = "විනාඩි {0}" + timeframes_only_distance["hour"] = "පැයක්" + timeframes_only_distance["hours"] = "පැය {0}" + timeframes_only_distance["day"] = "දවසක්" + timeframes_only_distance["days"] = "දවස් {0}" + timeframes_only_distance["week"] = "සතියක්" + timeframes_only_distance["weeks"] = "සති {0}" + timeframes_only_distance["month"] = "මාසයක්" + timeframes_only_distance["months"] = "මාස {0}" + timeframes_only_distance["year"] = "අවුරුද්දක්" + timeframes_only_distance["years"] = "අවුරුදු {0}" + + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + """ + Sinhala awares time frame format function, takes into account + the differences between general, past, and future forms (three different suffixes). + """ + abs_delta = abs(delta) + form = self.timeframes[timeframe] + + if isinstance(form, str): + return form.format(abs_delta) + + if delta > 0: + key = "future" + else: + key = "past" + form = form[key] + + return form.format(abs_delta) + + def describe( + self, + timeframe: TimeFrameLiteral, + delta: Union[float, int] = 1, # key is always future when only_distance=False + only_distance: bool = False, + ) -> str: + """Describes a delta within a timeframe in plain language. + + :param timeframe: a string representing a timeframe. + :param delta: a quantity representing a delta in a timeframe. + :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords + """ + + if not only_distance: + return super().describe(timeframe, delta, only_distance) + # Sinhala uses a different case without 'in' or 'ago' + humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) + + return humanized + + month_names = [ + "", + "ජනවාරි", + "පෙබරවාරි", + "මාර්තු", + "අප්‍රේල්", + "මැයි", + "ජූනි", + "ජූලි", + "අගෝස්තු", + "සැප්තැම්බර්", + "ඔක්තෝබර්", + "නොවැම්බර්", + "දෙසැම්බර්", + ] + + month_abbreviations = [ + "", + "ජන", + "පෙබ", + "මාර්", + "අප්‍රේ", + "මැයි", + "ජුනි", + "ජූලි", + "අගෝ", + "සැප්", + "ඔක්", + "නොවැ", + "දෙසැ", + ] + + day_names = [ + "", + "සදුදා", + "අඟහරැවදා", + "බදාදා", + "බ්‍රහස්‍පතින්‍දා", + "සිකුරාදා", + "සෙනසුරාදා", + "ඉරිදා", + ] + + day_abbreviations = [ + "", + "සදුද", + "බදා", + "බදා", + "සිකු", + "සෙන", + "අ", + "ඉරිදා", + ] diff --git a/tests/test_locales.py b/tests/test_locales.py index a69a085ce..f7aa17a90 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1922,3 +1922,77 @@ 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 TestSinhalaLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "දැන්" + assert self.locale._format_timeframe("second", -1) == "තත්පරයක" + 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", 4) == "මිනිත්තු 4 කින්" + + 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("week", -1) == "සතියක" + assert self.locale._format_timeframe("weeks", -10) == "සති 10 ක" + + assert self.locale._format_timeframe("month", -1) == "මාසයක" + assert self.locale._format_timeframe("months", -2) == "මාස 2 ක" + + assert self.locale._format_timeframe("year", 1) == "වසරක් තුළ" + assert self.locale._format_timeframe("years", -21) == "අවුරුදු 21 ක" + + def test_describe_si(self): + assert self.locale.describe("second", only_distance=True) == "තත්පරයක්" + assert ( + self.locale.describe("second", only_distance=False) == "තත්පරයකින්" + ) # (in) a second + + assert self.locale.describe("minute", only_distance=True) == "මිනිත්තුවක්" + assert ( + self.locale.describe("minute", only_distance=False) == "විනාඩියකින්" + ) # (in) a minute + + assert self.locale.describe("hour", only_distance=True) == "පැයක්" + assert self.locale.describe("hour", only_distance=False) == "පැයකින්" + + assert self.locale.describe("day", only_distance=True) == "දවසක්" + assert self.locale.describe("day", only_distance=False) == "දිනකට" + + assert self.locale.describe("week", only_distance=True) == "සතියක්" + assert self.locale.describe("week", only_distance=False) == "සතියකින්" + + assert self.locale.describe("month", only_distance=True) == "මාසයක්" + assert self.locale.describe("month", only_distance=False) == "එය මාසය තුළ" + + assert self.locale.describe("year", only_distance=True) == "අවුරුද්දක්" + assert self.locale.describe("year", only_distance=False) == "වසරක් තුළ" + + def test_format_relative_now(self): + result = self.locale._format_relative("දැන්", "now", 0) + 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 + + 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 8b4e5661caf09168de6484c611a007e0cb6bf75d Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sat, 2 Oct 2021 21:54:16 -0400 Subject: [PATCH 562/649] Fix pypy CI issues and begin testing against Python 3.10 (#1045) * Fix pypy CI issues and add Python 3.10 * Replace 3.10 with 3.10-dev and move pytz to 2021.1 --- .github/workflows/continuous_integration.yml | 4 ++-- requirements-dev.txt | 2 +- tox.ini | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index c3de5ffea..364cc773d 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -16,12 +16,12 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3", "3.6", "3.7", "3.8", "3.9"] + python-version: ["pypy-3.7", "3.6", "3.7", "3.8", "3.9", "3.10-dev"] os: [ubuntu-latest, macos-latest, windows-latest] exclude: # pypy3 randomly fails on Windows builds - os: windows-latest - python-version: "pypy3" + python-version: "pypy-3.7" steps: # Check out latest code diff --git a/requirements-dev.txt b/requirements-dev.txt index a04cbef76..32417b73f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ pytest==6.* pytest-cov==2.* pytest-mock==3.* python-dateutil>=2.7.0 -pytz==2021.* +pytz==2021.1 simplejson==3.* sphinx==4.* sphinx-autodoc-typehints==1.* diff --git a/tox.ini b/tox.ini index 7113f0d95..68b3240a0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,16 @@ [tox] minversion = 3.18.0 -envlist = py{py3,36,37,38,39} +envlist = py{py3,36,37,38,39,310} skip_missing_interpreters = true [gh-actions] python = - pypy3: pypy3 + pypy-3.7: pypy3 3.6: py36 3.7: py37 3.8: py38 3.9: py39 + 3.10: py310 [testenv] deps = -rrequirements-dev.txt From 496241694197c88a345a3094050bebaa7867da69 Mon Sep 17 00:00:00 2001 From: cyriaka90 Date: Tue, 5 Oct 2021 05:16:07 +0200 Subject: [PATCH 563/649] add Urdu locale (#1046) --- arrow/constants.py | 2 ++ arrow/locales.py | 81 +++++++++++++++++++++++++++++++++++++++++++ tests/test_arrow.py | 2 ++ tests/test_locales.py | 30 ++++++++++++++++ 4 files changed, 115 insertions(+) diff --git a/arrow/constants.py b/arrow/constants.py index 085ec392f..e41ffa5d1 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -143,4 +143,6 @@ "ta", "ta-in", "ta-lk", + "ur", + "ur-pk", } diff --git a/arrow/locales.py b/arrow/locales.py index 48413276f..32be55ab0 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -5806,3 +5806,84 @@ def describe( "අ", "ඉරිදා", ] + + +class UrduLocale(Locale): + + names = ["ur", "ur-pk"] + + past = "پہلے {0}" + future = "میں {0}" + and_word = "اور" + + 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 = [ + "", + "جنوری", + "فروری", + "مارچ", + "اپریل", + "مئی", + "جون", + "جولائی", + "اگست", + "ستمبر", + "اکتوبر", + "نومبر", + "دسمبر", + ] + + month_abbreviations = [ + "", + "جنوری", + "فروری", + "مارچ", + "اپریل", + "مئی", + "جون", + "جولائی", + "اگست", + "ستمبر", + "اکتوبر", + "نومبر", + "دسمبر", + ] + + day_names = [ + "", + "سوموار", + "منگل", + "بدھ", + "جمعرات", + "جمعہ", + "ہفتہ", + "اتوار", + ] + + day_abbreviations = [ + "", + "سوموار", + "منگل", + "بدھ", + "جمعرات", + "جمعہ", + "ہفتہ", + "اتوار", + ] diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 732cd79c2..0314ffae8 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2420,6 +2420,8 @@ def locale_list_no_weeks(): "ta", "ta-in", "ta-lk", + "ur", + "ur-pk", ] return tested_langs diff --git a/tests/test_locales.py b/tests/test_locales.py index f7aa17a90..0401f7bfb 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1422,6 +1422,36 @@ def test_weekday_and_month(self): assert self.locale.month_abbreviation(dt.isoweekday()) == "qer" +@pytest.mark.usefixtures("lang_locale") +class TestUrduLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "ابھی" + 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("minute", 1) == "ایک منٹ" + assert self.locale._format_timeframe("minutes", -4) == "4 منٹ" + 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("week", 1) == "ایک ہفتے" + assert self.locale._format_timeframe("weeks", -12) == "12 ہفتے" + assert self.locale._format_timeframe("month", 1) == "ایک مہینہ" + assert self.locale._format_timeframe("months", -2) == "2 ماہ" + assert self.locale._format_timeframe("year", 1) == "ایک سال" + assert self.locale._format_timeframe("years", -2) == "2 سال" + + def test_weekday_and_month(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + # Saturday + assert self.locale.day_name(dt.isoweekday()) == "ہفتہ" + assert self.locale.day_abbreviation(dt.isoweekday()) == "ہفتہ" + # June + assert self.locale.month_name(dt.isoweekday()) == "جون" + assert self.locale.month_abbreviation(dt.isoweekday()) == "جون" + + @pytest.mark.usefixtures("lang_locale") class TestEstonianLocale: def test_format_timeframe(self): From 10e8970baabeefd8c2f31814d66b11421ac128f1 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Thu, 14 Oct 2021 23:36:44 -0400 Subject: [PATCH 564/649] Add support for Python 3.10 (#1049) --- .github/workflows/continuous_integration.yml | 8 ++++---- Makefile | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 364cc773d..140bf4466 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.7", "3.6", "3.7", "3.8", "3.9", "3.10-dev"] + python-version: ["pypy-3.7", "3.6", "3.7", "3.8", "3.9", "3.10"] os: [ubuntu-latest, macos-latest, windows-latest] exclude: # pypy3 randomly fails on Windows builds @@ -73,7 +73,7 @@ jobs: # Upload coverage report - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v2 with: file: coverage.xml @@ -85,10 +85,10 @@ jobs: - uses: actions/checkout@v2 # Set up Python - - name: Set up Python 3.9 + - name: Set up Python 3.10 uses: actions/setup-python@v2 with: - python-version: "3.9" + python-version: "3.10" # Configure pip cache - name: Cache pip diff --git a/Makefile b/Makefile index efdcb2d7b..c473e3f13 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,9 @@ 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 -build36 build37 build38 build39: clean +build36 build37 build38 build39 build310: clean $(PYTHON_VER) -m venv venv . venv/bin/activate; \ pip install -U pip setuptools wheel; \ From c35134b62916f517c31f32b7eae56736f205a5f3 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Fri, 15 Oct 2021 18:02:24 -0400 Subject: [PATCH 565/649] Update dependencies for Python 3.10 (#1050) * Update dependencies for Python 3.10 * Update flake8 pre-commit --- .pre-commit-config.yaml | 10 +++++----- requirements-dev.txt | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1eac547cb..70c19ac6b 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: v2.23.3 + rev: v2.29.0 hooks: - id: pyupgrade args: [--py36-plus] @@ -34,17 +34,17 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/psf/black - rev: 21.7b0 + rev: 21.9b0 hooks: - id: black args: [--safe, --quiet, --target-version=py36] - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + - repo: https://github.com/pycqa/flake8 + rev: 4.0.1 hooks: - id: flake8 additional_dependencies: [flake8-bugbear] - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.910' + rev: 'v0.910-1' hooks: - id: mypy additional_dependencies: [types-python-dateutil] diff --git a/requirements-dev.txt b/requirements-dev.txt index 32417b73f..75f443410 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,7 @@ dateparser==1.* pre-commit==2.* pytest==6.* -pytest-cov==2.* +pytest-cov==3.* pytest-mock==3.* python-dateutil>=2.7.0 pytz==2021.1 From 8151564f5424b480a6423a36521837d36dde7b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Slobodan=20Markovi=C4=87?= Date: Wed, 20 Oct 2021 03:44:30 +0200 Subject: [PATCH 566/649] Corrections in Serbian locale (#1047) Corrections are necessary to avoid errors in matching Serbian dates (e.g. parsing "31. jul 2021." is now impossible with both "DD. MMM YYYY." and "DD. MMMM YYYY." patterns). Co-authored-by: Jad Chaar Co-authored-by: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> --- arrow/locales.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 32be55ab0..68119bb1e 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -5211,13 +5211,13 @@ class SerbianLocale(Locale): month_names = [ "", - "januar", # Јануар + "januar", # јануар "februar", # фебруар "mart", # март "april", # април "maj", # мај - "juni", # јун - "juli", # јул + "jun", # јун + "jul", # јул "avgust", # август "septembar", # септембар "oktobar", # октобар @@ -5227,18 +5227,18 @@ class SerbianLocale(Locale): month_abbreviations = [ "", - "jan.", - "febr.", - "mart", - "april", + "jan", + "feb", + "mar", + "apr", "maj", - "juni", - "juli", - "avg.", - "sept.", - "okt.", - "nov.", - "dec.", + "jun", + "jul", + "avg", + "sep", + "okt", + "nov", + "dec", ] day_names = [ From 1e69992cf1df71c46669e8d1b57e0646f90e5f0a Mon Sep 17 00:00:00 2001 From: Kian Meng Ang Date: Wed, 20 Oct 2021 10:34:27 +0800 Subject: [PATCH 567/649] Fix typos (#1051) Co-authored-by: Jad Chaar --- CHANGELOG.rst | 2 +- tests/test_parser.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0294bad7e..132742d57 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -358,7 +358,7 @@ The following will work in v0.15.0: ------------------- - [NEW] Added support for ``week`` granularity in ``Arrow.humanize()``. For example, ``arrow.utcnow().shift(weeks=-1).humanize(granularity="week")`` outputs "a week ago". This change introduced two new untranslated words, ``week`` and ``weeks``, to all locale dictionaries, so locale contributions are welcome! -- [NEW] Fully translated the Brazilian Portugese locale. +- [NEW] Fully translated the Brazilian Portuguese locale. - [CHANGE] Updated the Macedonian locale to inherit from a Slavic base. - [FIX] Fixed a bug that caused ``arrow.get()`` to ignore tzinfo arguments of type string (e.g. ``arrow.get(tzinfo="Europe/Paris")``). - [FIX] Fixed a bug that occurred when ``arrow.Arrow()`` was instantiated with a ``pytz`` tzinfo object. diff --git a/tests/test_parser.py b/tests/test_parser.py index 4552f65a2..4a4cfe41a 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1323,7 +1323,7 @@ def test_iso8601_basic_format(self): # ordinal in basic format: YYYYDDDD assert self.parser.parse_iso("1998136") == datetime(1998, 5, 16) - # timezone requires +- seperator + # timezone requires +- separator with pytest.raises(ParserError): self.parser.parse_iso("20180517T1055130700") From 2ee879f771e72dd6855f3295892585494ec4d864 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 22 Oct 2021 22:59:18 +0300 Subject: [PATCH 568/649] Declare support for Python 3.10 (#1053) --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index df0a5a759..350a5a0f8 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ], keywords="arrow date time datetime timestamp timezone humanize", project_urls={ From 4d1aa4f9dfeaa377d5cdfc9ca52a2a473d7e6f6d Mon Sep 17 00:00:00 2001 From: Sania D Souza Date: Fri, 22 Oct 2021 19:47:47 -0400 Subject: [PATCH 569/649] Issue 985 -- Add Azerbaijani second time frame (#1052) * Changes to Az time frames for second and week,weeks * Added tests for the Azerbaijani locale similar to the Turkish locale * Fixed AZ tests * Fixed 'a year' in locales.py * ' bir il' -> 'bir il' Co-authored-by: Jad Chaar --- arrow/locales.py | 6 ++-- tests/test_locales.py | 70 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 68119bb1e..0b33b8c1e 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2447,7 +2447,7 @@ class AzerbaijaniLocale(Locale): timeframes = { "now": "indi", - "second": "saniyə", + "second": "bir saniyə", "seconds": "{0} saniyə", "minute": "bir dəqiqə", "minutes": "{0} dəqiqə", @@ -2455,9 +2455,11 @@ class AzerbaijaniLocale(Locale): "hours": "{0} saat", "day": "bir gün", "days": "{0} gün", + "week": "bir həftə", + "weeks": "{0} həftə", "month": "bir ay", "months": "{0} ay", - "year": "il", + "year": "bir il", "years": "{0} il", } diff --git a/tests/test_locales.py b/tests/test_locales.py index 0401f7bfb..04a9a4524 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -768,6 +768,76 @@ def test_describe_multi(self): assert describe(seconds60, only_distance=True) == "דקה" +@pytest.mark.usefixtures("lang_locale") +class TestAzerbaijaniLocale: + def test_singles_mk(self): + assert self.locale._format_timeframe("second", 1) == "bir saniyə" + assert self.locale._format_timeframe("minute", 1) == "bir dəqiqə" + assert self.locale._format_timeframe("hour", 1) == "bir saat" + assert self.locale._format_timeframe("day", 1) == "bir gün" + assert self.locale._format_timeframe("week", 1) == "bir həftə" + assert self.locale._format_timeframe("month", 1) == "bir ay" + assert self.locale._format_timeframe("year", 1) == "bir il" + + def test_describe_mk(self): + assert self.locale.describe("second", only_distance=True) == "bir saniyə" + assert self.locale.describe("second", only_distance=False) == "bir saniyə sonra" + assert self.locale.describe("minute", only_distance=True) == "bir dəqiqə" + assert self.locale.describe("minute", only_distance=False) == "bir dəqiqə sonra" + assert self.locale.describe("hour", only_distance=True) == "bir saat" + assert self.locale.describe("hour", only_distance=False) == "bir saat sonra" + assert self.locale.describe("day", only_distance=True) == "bir gün" + assert self.locale.describe("day", only_distance=False) == "bir gün sonra" + assert self.locale.describe("week", only_distance=True) == "bir həftə" + assert self.locale.describe("week", only_distance=False) == "bir həftə sonra" + assert self.locale.describe("month", only_distance=True) == "bir ay" + assert self.locale.describe("month", only_distance=False) == "bir ay sonra" + assert self.locale.describe("year", only_distance=True) == "bir il" + assert self.locale.describe("year", only_distance=False) == "bir il sonra" + + def test_relative_mk(self): + assert self.locale._format_relative("indi", "now", 0) == "indi" + assert ( + self.locale._format_relative("1 saniyə", "seconds", 1) == "1 saniyə sonra" + ) + assert ( + self.locale._format_relative("1 saniyə", "seconds", -1) == "1 saniyə əvvəl" + ) + assert ( + self.locale._format_relative("1 dəqiqə", "minutes", 1) == "1 dəqiqə sonra" + ) + assert ( + self.locale._format_relative("1 dəqiqə", "minutes", -1) == "1 dəqiqə əvvəl" + ) + assert self.locale._format_relative("1 saat", "hours", 1) == "1 saat sonra" + assert self.locale._format_relative("1 saat", "hours", -1) == "1 saat əvvəl" + assert self.locale._format_relative("1 gün", "days", 1) == "1 gün sonra" + assert self.locale._format_relative("1 gün", "days", -1) == "1 gün əvvəl" + assert self.locale._format_relative("1 hafta", "weeks", 1) == "1 hafta sonra" + assert self.locale._format_relative("1 hafta", "weeks", -1) == "1 hafta əvvəl" + assert self.locale._format_relative("1 ay", "months", 1) == "1 ay sonra" + assert self.locale._format_relative("1 ay", "months", -1) == "1 ay əvvəl" + assert self.locale._format_relative("1 il", "years", 1) == "1 il sonra" + assert self.locale._format_relative("1 il", "years", -1) == "1 il əvvəl" + + def test_plurals_mk(self): + assert self.locale._format_timeframe("now", 0) == "indi" + assert self.locale._format_timeframe("second", 1) == "bir saniyə" + assert self.locale._format_timeframe("seconds", 30) == "30 saniyə" + assert self.locale._format_timeframe("minute", 1) == "bir dəqiqə" + assert self.locale._format_timeframe("minutes", 40) == "40 dəqiqə" + assert self.locale._format_timeframe("hour", 1) == "bir saat" + assert self.locale._format_timeframe("hours", 23) == "23 saat" + assert self.locale._format_timeframe("day", 1) == "bir gün" + assert self.locale._format_timeframe("days", 12) == "12 gün" + assert self.locale._format_timeframe("week", 1) == "bir həftə" + assert self.locale._format_timeframe("weeks", 38) == "38 həftə" + assert self.locale._format_timeframe("month", 1) == "bir ay" + assert self.locale._format_timeframe("months", 11) == "11 ay" + assert self.locale._format_timeframe("year", 1) == "bir il" + assert self.locale._format_timeframe("years", 12) == "12 il" + + @pytest.mark.usefixtures("lang_locale") class TestMarathiLocale: def test_dateCoreFunctionality(self): From f7a3aa3225eb646fec9156e8e39dc33750f2a227 Mon Sep 17 00:00:00 2001 From: Mohammed Ali Zubair Date: Sat, 23 Oct 2021 05:57:38 +0600 Subject: [PATCH 570/649] Add quarter granularity (#1048) * Add quarter granularity * rebase and fix * add more cases for quarters Co-authored-by: Mohammed Ali Zubair Co-authored-by: Jad Chaar --- arrow/arrow.py | 10 ++++++++-- arrow/locales.py | 6 ++++++ tests/test_arrow.py | 20 ++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index ad95cacdf..d6ed7a881 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -75,6 +75,7 @@ "day", "week", "month", + "quarter", "year", ] @@ -132,6 +133,7 @@ class Arrow: _SECS_PER_DAY: Final[int] = 60 * 60 * 24 _SECS_PER_WEEK: Final[int] = 60 * 60 * 24 * 7 _SECS_PER_MONTH: Final[float] = 60 * 60 * 24 * 30.5 + _SECS_PER_QUARTER: Final[float] = 60 * 60 * 24 * 30.5 * 3 _SECS_PER_YEAR: Final[int] = 60 * 60 * 24 * 365 _SECS_MAP: Final[Mapping[TimeFrameLiteral, float]] = { @@ -141,6 +143,7 @@ class Arrow: "day": _SECS_PER_DAY, "week": _SECS_PER_WEEK, "month": _SECS_PER_MONTH, + "quarter": _SECS_PER_QUARTER, "year": _SECS_PER_YEAR, } @@ -1245,12 +1248,14 @@ def humanize( delta = sign * delta_second / self._SECS_PER_WEEK elif granularity == "month": delta = sign * delta_second / self._SECS_PER_MONTH + elif granularity == "quarter": + delta = sign * delta_second / self._SECS_PER_QUARTER elif granularity == "year": delta = sign * delta_second / self._SECS_PER_YEAR else: raise ValueError( "Invalid level of granularity. " - "Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'." + "Please select between 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter' or 'year'." ) if trunc(abs(delta)) != 1: @@ -1275,6 +1280,7 @@ def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float: delta = float(delta_second) frames: Tuple[TimeFrameLiteral, ...] = ( "year", + "quarter", "month", "week", "day", @@ -1288,7 +1294,7 @@ def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float: if len(timeframes) < len(granularity): raise ValueError( "Invalid level of granularity. " - "Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'." + "Please select between 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter' or 'year'." ) return locale.describe_multi(timeframes, only_distance=only_distance) diff --git a/arrow/locales.py b/arrow/locales.py index 0b33b8c1e..d6d5c4869 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -35,6 +35,8 @@ "weeks", "month", "months", + "quarter", + "quarters", "year", "years", ] @@ -98,6 +100,8 @@ class Locale: "weeks": "", "month": "", "months": "", + "quarter": "", + "quarters": "", "year": "", "years": "", } @@ -314,6 +318,8 @@ class EnglishLocale(Locale): "weeks": "{0} weeks", "month": "a month", "months": "{0} months", + "quarter": "a quarter", + "quarters": "{0} quarters", "year": "a year", "years": "{0} years", } diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 0314ffae8..589f91146 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -1935,9 +1935,29 @@ def test_granularity(self): assert later506.humanize(self.now, granularity="week") == "in 82 weeks" assert self.now.humanize(later506, granularity="month") == "18 months ago" assert later506.humanize(self.now, granularity="month") == "in 18 months" + assert self.now.humanize(later506, granularity="quarter") == "6 quarters ago" + assert later506.humanize(self.now, granularity="quarter") == "in 6 quarters" assert self.now.humanize(later506, granularity="year") == "a year ago" assert later506.humanize(self.now, granularity="year") == "in a year" + 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) + 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) + 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) + 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) + 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) + 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) assert self.now.humanize(later108, granularity="year") == "3 years ago" assert later108.humanize(self.now, granularity="year") == "in 3 years" From cc1cbeb01878b0985aba8f6ec4e3a7a748b527a6 Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Sun, 24 Oct 2021 17:56:06 -0400 Subject: [PATCH 571/649] Added Error Checking For Empty Granularity List (#1016) * Added Error Checking * Updated Error Message To Be More Descriptive Co-authored-by: Chris <30196510+systemcatch@users.noreply.github.com> * Improved Error Message String Co-authored-by: Anish Nyayachavadi Co-authored-by: Chris <30196510+systemcatch@users.noreply.github.com> Co-authored-by: Jad Chaar --- arrow/arrow.py | 7 +++++++ tests/test_arrow.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/arrow/arrow.py b/arrow/arrow.py index d6ed7a881..fef66c103 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1263,6 +1263,13 @@ def humanize( return locale.describe(granularity, delta, only_distance=only_distance) else: + + if not granularity: + raise ValueError( + "Empty granularity list provided. " + "Please select one or more from 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year'." + ) + timeframes: List[Tuple[TimeFrameLiteral, float]] = [] def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float: diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 589f91146..4c9ca110e 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2286,6 +2286,13 @@ def test_untranslated_granularity(self, mocker): with pytest.raises(ValueError): arw.humanize(later, granularity="week") + def test_empty_granularity_list(self): + arw = arrow.Arrow(2013, 1, 1, 0, 0, 0) + later = arw.shift(seconds=55000) + + with pytest.raises(ValueError): + 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 From 3e50ae4b6c58e36bd32a672ce073f4c64d5bf182 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Sun, 24 Oct 2021 18:13:48 -0400 Subject: [PATCH 572/649] Bump version to 1.2.1 and update CHANGELOG. (#1055) --- CHANGELOG.rst | 25 +++++++++++++++++++++++++ arrow/_version.py | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 132742d57..87a1ef2bc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,31 @@ Changelog ========= +1.2.1 (2021-10-24) +------------------ + +- [NEW] Added quarter granularity to humanize, for example: + +.. code-block:: python + + >>> import arrow + >>> now = arrow.now() + >>> four_month_shift = now.shift(months=4) + >>> now.humanize(four_month_shift, granularity="quarter") + 'a quarter ago' + >>> four_month_shift.humanize(now, granularity="quarter") + 'in a quarter' + >>> thirteen_month_shift = now.shift(months=13) + >>> thirteen_month_shift.humanize(now, granularity="quarter") + 'in 4 quarters' + >>> now.humanize(thirteen_month_shift, granularity="quarter") + '4 quarters ago' + +- [NEW] Added Sinhala and Urdu locales. +- [NEW] Added official support for Python 3.10. +- [CHANGED] Updated Azerbaijani, Hebrew, and Serbian locales and added tests. +- [CHANGED] Passing an empty granularity list to ``humanize`` now raises a ``ValueError``. + 1.2.0 (2021-09-12) ------------------ diff --git a/arrow/_version.py b/arrow/_version.py index c68196d1c..a955fdae1 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "1.2.0" +__version__ = "1.2.1" From baebfff4d1bb64b9cbf9be10a625c69abc5a1af7 Mon Sep 17 00:00:00 2001 From: manlix Date: Fri, 29 Oct 2021 06:35:25 +0300 Subject: [PATCH 573/649] Incorrect plural for seconds in Russian (#1056) (#1057) --- arrow/locales.py | 8 +++-- tests/test_arrow.py | 3 +- tests/test_locales.py | 83 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index d6d5c4869..1a7a635f3 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1487,8 +1487,12 @@ class RussianLocale(SlavicBaseLocale): timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "сейчас", - "second": "Второй", - "seconds": "{0} несколько секунд", + "second": "секунда", + "seconds": { + "singular": "{0} секунду", + "dual": "{0} секунды", + "plural": "{0} секунд", + }, "minute": "минуту", "minutes": { "singular": "{0} минуту", diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 4c9ca110e..02b42a5a1 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2327,8 +2327,7 @@ def test_seconds(self): arw = arrow.Arrow(2013, 1, 1, 0, 0, 44) result = arw.humanize(self.datetime, locale="ru") - - assert result == "через 44 несколько секунд" + assert result == "через 44 секунды" def test_years(self): diff --git a/tests/test_locales.py b/tests/test_locales.py index 04a9a4524..133b76e36 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -252,6 +252,89 @@ def test_month_abbreviation(self): @pytest.mark.usefixtures("lang_locale") class TestRussianLocale: + def test_singles_timeframe(self): + # Second + result = self.locale._format_timeframe("second", 1) + assert result == "секунда" + + result = self.locale._format_timeframe("second", -1) + assert result == "секунда" + + def test_singles_relative(self): + # Second in the future + result = self.locale._format_relative("секунду", "second", 1) + assert result == "через секунду" + + # Second in the past + result = self.locale._format_relative("секунду", "second", -1) + assert result == "секунду назад" + + def test_plurals_timeframe(self): + # Seconds in the future + result = self.locale._format_timeframe("seconds", 2) + assert result == "2 секунды" + + result = self.locale._format_timeframe("seconds", 5) + assert result == "5 секунд" + + result = self.locale._format_timeframe("seconds", 21) + assert result == "21 секунду" + + result = self.locale._format_timeframe("seconds", 22) + assert result == "22 секунды" + + result = self.locale._format_timeframe("seconds", 25) + assert result == "25 секунд" + + # Seconds in the past + result = self.locale._format_timeframe("seconds", -2) + assert result == "2 секунды" + + result = self.locale._format_timeframe("seconds", -5) + assert result == "5 секунд" + + result = self.locale._format_timeframe("seconds", -21) + assert result == "21 секунду" + + result = self.locale._format_timeframe("seconds", -22) + assert result == "22 секунды" + + result = self.locale._format_timeframe("seconds", -25) + assert result == "25 секунд" + + def test_plurals_relative(self): + # Seconds in the future + result = self.locale._format_relative("1 секунду", "seconds", 1) + assert result == "через 1 секунду" + + result = self.locale._format_relative("2 секунды", "seconds", 2) + assert result == "через 2 секунды" + + result = self.locale._format_relative("5 секунд", "seconds", 5) + assert result == "через 5 секунд" + + result = self.locale._format_relative("21 секунду", "seconds", 21) + assert result == "через 21 секунду" + + result = self.locale._format_relative("25 секунд", "seconds", 25) + assert result == "через 25 секунд" + + # Seconds in the past + result = self.locale._format_relative("1 секунду", "seconds", -1) + assert result == "1 секунду назад" + + result = self.locale._format_relative("2 секунды", "seconds", -2) + assert result == "2 секунды назад" + + result = self.locale._format_relative("5 секунд", "seconds", -5) + assert result == "5 секунд назад" + + result = self.locale._format_relative("21 секунда", "seconds", -21) + assert result == "21 секунда назад" + + result = self.locale._format_relative("25 секунд", "seconds", -25) + assert result == "25 секунд назад" + def test_plurals2(self): assert self.locale._format_timeframe("hours", 0) == "0 часов" assert self.locale._format_timeframe("hours", 1) == "1 час" From 675c877a2512fa7659baf9e38a97f7382a011790 Mon Sep 17 00:00:00 2001 From: Aiman Date: Sun, 31 Oct 2021 00:02:05 +0800 Subject: [PATCH 574/649] Add Qeeks and Quarters Timeframes for Indonesian locale (#1060) --- arrow/locales.py | 4 ++++ tests/test_locales.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 1a7a635f3..6a0cd56f0 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -4233,8 +4233,12 @@ class IndonesianLocale(Locale): "hours": "{0} jam", "day": "1 hari", "days": "{0} hari", + "week": "1 minggu", + "weeks": "{0} minggu", "month": "1 bulan", "months": "{0} bulan", + "quarter": "1 kuartal", + "quarters": "{0} kuartal", "year": "1 tahun", "years": "{0} tahun", } diff --git a/tests/test_locales.py b/tests/test_locales.py index 133b76e36..c1a52252a 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1165,6 +1165,9 @@ def test_timeframes(self): assert self.locale._format_timeframe("days", 3) == "3 hari" assert self.locale._format_timeframe("years", 5) == "5 tahun" + assert self.locale._format_timeframe("weeks", 2) == "2 minggu" + assert self.locale._format_timeframe("quarters", 3) == "3 kuartal" + def test_format_relative_now(self): assert self.locale._format_relative("baru saja", "now", 0) == "baru saja" From 8f5b21f037abafc4cd11cddc5abbd4aae7e897e6 Mon Sep 17 00:00:00 2001 From: manlix Date: Tue, 2 Nov 2021 03:59:34 +0300 Subject: [PATCH 575/649] Add Support for Quarter Granularity in Russian (#1062) (#1063) --- arrow/locales.py | 6 ++++ tests/test_locales.py | 79 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index 6a0cd56f0..b7b744c1a 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -1515,6 +1515,12 @@ class RussianLocale(SlavicBaseLocale): "dual": "{0} месяца", "plural": "{0} месяцев", }, + "quarter": "квартал", + "quarters": { + "singular": "{0} квартал", + "dual": "{0} квартала", + "plural": "{0} кварталов", + }, "year": "год", "years": {"singular": "{0} год", "dual": "{0} года", "plural": "{0} лет"}, } diff --git a/tests/test_locales.py b/tests/test_locales.py index c1a52252a..48c106133 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -260,6 +260,13 @@ def test_singles_timeframe(self): result = self.locale._format_timeframe("second", -1) assert result == "секунда" + # Quarter + result = self.locale._format_timeframe("quarter", 1) + assert result == "квартал" + + result = self.locale._format_timeframe("quarter", -1) + assert result == "квартал" + def test_singles_relative(self): # Second in the future result = self.locale._format_relative("секунду", "second", 1) @@ -269,6 +276,14 @@ def test_singles_relative(self): result = self.locale._format_relative("секунду", "second", -1) assert result == "секунду назад" + # Quarter in the future + result = self.locale._format_relative("квартал", "quarter", 1) + assert result == "через квартал" + + # Quarter in the past + result = self.locale._format_relative("квартал", "quarter", -1) + assert result == "квартал назад" + def test_plurals_timeframe(self): # Seconds in the future result = self.locale._format_timeframe("seconds", 2) @@ -302,6 +317,38 @@ def test_plurals_timeframe(self): result = self.locale._format_timeframe("seconds", -25) assert result == "25 секунд" + # Quarters in the future + result = self.locale._format_timeframe("quarters", 2) + assert result == "2 квартала" + + result = self.locale._format_timeframe("quarters", 5) + assert result == "5 кварталов" + + result = self.locale._format_timeframe("quarters", 21) + assert result == "21 квартал" + + result = self.locale._format_timeframe("quarters", 22) + assert result == "22 квартала" + + result = self.locale._format_timeframe("quarters", 25) + assert result == "25 кварталов" + + # Quarters in the past + result = self.locale._format_timeframe("quarters", -2) + assert result == "2 квартала" + + result = self.locale._format_timeframe("quarters", -5) + assert result == "5 кварталов" + + result = self.locale._format_timeframe("quarters", -21) + assert result == "21 квартал" + + result = self.locale._format_timeframe("quarters", -22) + assert result == "22 квартала" + + result = self.locale._format_timeframe("quarters", -25) + assert result == "25 кварталов" + def test_plurals_relative(self): # Seconds in the future result = self.locale._format_relative("1 секунду", "seconds", 1) @@ -335,6 +382,38 @@ def test_plurals_relative(self): result = self.locale._format_relative("25 секунд", "seconds", -25) assert result == "25 секунд назад" + # Quarters in the future + result = self.locale._format_relative("1 квартал", "quarters", 1) + assert result == "через 1 квартал" + + result = self.locale._format_relative("2 квартала", "quarters", 2) + assert result == "через 2 квартала" + + result = self.locale._format_relative("5 кварталов", "quarters", 5) + assert result == "через 5 кварталов" + + result = self.locale._format_relative("21 квартал", "quarters", 21) + assert result == "через 21 квартал" + + result = self.locale._format_relative("25 кварталов", "quarters", 25) + assert result == "через 25 кварталов" + + # Quarters in the past + result = self.locale._format_relative("1 квартал", "quarters", -1) + assert result == "1 квартал назад" + + result = self.locale._format_relative("2 квартала", "quarters", -2) + assert result == "2 квартала назад" + + result = self.locale._format_relative("5 кварталов", "quarters", -5) + assert result == "5 кварталов назад" + + result = self.locale._format_relative("21 квартал", "quarters", -21) + assert result == "21 квартал назад" + + result = self.locale._format_relative("25 кварталов", "quarters", -25) + assert result == "25 кварталов назад" + def test_plurals2(self): assert self.locale._format_timeframe("hours", 0) == "0 часов" assert self.locale._format_timeframe("hours", 1) == "1 час" From 98bb910e594ace7e9be790e385ce8afb3ba4dbe5 Mon Sep 17 00:00:00 2001 From: Noah H Date: Fri, 5 Nov 2021 18:49:26 -0400 Subject: [PATCH 576/649] add flake8-annotations package for type linting (#1061) * add flake8-annotations package for type linting - add pkg in requirements-dev.txt - require pkg in pre-commit config - add some easily resolved annotations in tests - ignore some type warnings in tox.ini * fix python version incompatibility (list vs typing.List) * remove flake8-annotations from pip requirements Co-authored-by: Jad Chaar Co-authored-by: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- tests/test_arrow.py | 41 +++++++++++++++++++++-------------------- tox.ini | 4 ++-- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 70c19ac6b..c65b26187 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: rev: 4.0.1 hooks: - id: flake8 - additional_dependencies: [flake8-bugbear] + additional_dependencies: [flake8-bugbear,flake8-annotations] - repo: https://github.com/pre-commit/mirrors-mypy rev: 'v0.910-1' hooks: diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 02b42a5a1..00d633cfa 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2,6 +2,7 @@ import sys import time from datetime import date, datetime, timedelta +from typing import List import dateutil import pytest @@ -2340,7 +2341,7 @@ def test_years(self): # Fixtures for Dehumanize @pytest.fixture(scope="class") -def locale_list_no_weeks(): +def locale_list_no_weeks() -> List[str]: tested_langs = [ "en", "en-us", @@ -2454,7 +2455,7 @@ def locale_list_no_weeks(): @pytest.fixture(scope="class") -def locale_list_with_weeks(): +def locale_list_with_weeks() -> List[str]: tested_langs = [ "en", "en-us", @@ -2516,7 +2517,7 @@ def locale_list_with_weeks(): class TestArrowDehumanize: - def test_now(self, locale_list_no_weeks): + def test_now(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: @@ -2534,7 +2535,7 @@ def test_now(self, locale_list_no_weeks): assert arw.dehumanize(second_ago_string, locale=lang) == arw assert arw.dehumanize(second_future_string, locale=lang) == arw - def test_seconds(self, locale_list_no_weeks): + def test_seconds(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: @@ -2552,7 +2553,7 @@ def test_seconds(self, locale_list_no_weeks): assert arw.dehumanize(second_ago_string, locale=lang) == second_ago assert arw.dehumanize(second_future_string, locale=lang) == second_future - def test_minute(self, locale_list_no_weeks): + def test_minute(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: @@ -2570,7 +2571,7 @@ def test_minute(self, locale_list_no_weeks): assert arw.dehumanize(minute_ago_string, locale=lang) == minute_ago assert arw.dehumanize(minute_future_string, locale=lang) == minute_future - def test_minutes(self, locale_list_no_weeks): + def test_minutes(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: @@ -2588,7 +2589,7 @@ def test_minutes(self, locale_list_no_weeks): assert arw.dehumanize(minute_ago_string, locale=lang) == minute_ago assert arw.dehumanize(minute_future_string, locale=lang) == minute_future - def test_hour(self, locale_list_no_weeks): + def test_hour(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: @@ -2604,7 +2605,7 @@ def test_hour(self, locale_list_no_weeks): assert arw.dehumanize(hour_ago_string, locale=lang) == hour_ago assert arw.dehumanize(hour_future_string, locale=lang) == hour_future - def test_hours(self, locale_list_no_weeks): + def test_hours(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: @@ -2620,7 +2621,7 @@ def test_hours(self, locale_list_no_weeks): assert arw.dehumanize(hour_ago_string, locale=lang) == hour_ago assert arw.dehumanize(hour_future_string, locale=lang) == hour_future - def test_week(self, locale_list_with_weeks): + def test_week(self, locale_list_with_weeks: List[str]): for lang in locale_list_with_weeks: @@ -2636,7 +2637,7 @@ def test_week(self, locale_list_with_weeks): assert arw.dehumanize(week_ago_string, locale=lang) == week_ago assert arw.dehumanize(week_future_string, locale=lang) == week_future - def test_weeks(self, locale_list_with_weeks): + def test_weeks(self, locale_list_with_weeks: List[str]): for lang in locale_list_with_weeks: @@ -2652,7 +2653,7 @@ def test_weeks(self, locale_list_with_weeks): assert arw.dehumanize(week_ago_string, locale=lang) == week_ago assert arw.dehumanize(week_future_string, locale=lang) == week_future - def test_year(self, locale_list_no_weeks): + def test_year(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: @@ -2668,7 +2669,7 @@ def test_year(self, locale_list_no_weeks): assert arw.dehumanize(year_ago_string, locale=lang) == year_ago assert arw.dehumanize(year_future_string, locale=lang) == year_future - def test_years(self, locale_list_no_weeks): + def test_years(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: @@ -2684,7 +2685,7 @@ def test_years(self, locale_list_no_weeks): assert arw.dehumanize(year_ago_string, locale=lang) == year_ago assert arw.dehumanize(year_future_string, locale=lang) == year_future - def test_gt_than_10_years(self, locale_list_no_weeks): + def test_gt_than_10_years(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: @@ -2700,7 +2701,7 @@ def test_gt_than_10_years(self, locale_list_no_weeks): assert arw.dehumanize(year_ago_string, locale=lang) == year_ago assert arw.dehumanize(year_future_string, locale=lang) == year_future - def test_mixed_granularity(self, locale_list_no_weeks): + def test_mixed_granularity(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: @@ -2718,7 +2719,7 @@ def test_mixed_granularity(self, locale_list_no_weeks): assert arw.dehumanize(past_string, locale=lang) == past assert arw.dehumanize(future_string, locale=lang) == future - def test_mixed_granularity_hours(self, locale_list_no_weeks): + def test_mixed_granularity_hours(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: @@ -2736,7 +2737,7 @@ def test_mixed_granularity_hours(self, locale_list_no_weeks): assert arw.dehumanize(past_string, locale=lang) == past assert arw.dehumanize(future_string, locale=lang) == future - def test_mixed_granularity_day(self, locale_list_no_weeks): + def test_mixed_granularity_day(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: @@ -2754,7 +2755,7 @@ def test_mixed_granularity_day(self, locale_list_no_weeks): assert arw.dehumanize(past_string, locale=lang) == past assert arw.dehumanize(future_string, locale=lang) == future - def test_mixed_granularity_day_hour(self, locale_list_no_weeks): + def test_mixed_granularity_day_hour(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: @@ -2811,7 +2812,7 @@ def test_normalized_locale(self): assert arw.dehumanize(second_future_string, locale="zh_hk") == second_future # Ensures relative units are required in string - def test_require_relative_unit(self, locale_list_no_weeks): + def test_require_relative_unit(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: @@ -2833,7 +2834,7 @@ def test_require_relative_unit(self, locale_list_no_weeks): arw.dehumanize(second_future_string, locale=lang) # Test for scrambled input - def test_scrambled_input(self, locale_list_no_weeks): + def test_scrambled_input(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: @@ -2861,7 +2862,7 @@ def test_scrambled_input(self, locale_list_no_weeks): with pytest.raises(ValueError): arw.dehumanize(second_future_string, locale=lang) - def test_no_units_modified(self, locale_list_no_weeks): + def test_no_units_modified(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: diff --git a/tox.ini b/tox.ini index 68b3240a0..b77461760 100644 --- a/tox.ini +++ b/tox.ini @@ -49,5 +49,5 @@ multi_line_output = 3 include_trailing_comma = true [flake8] -per-file-ignores = arrow/__init__.py:F401 -ignore = E203,E501,W503 +per-file-ignores = arrow/__init__.py:F401,tests/*:ANN001,ANN201 +ignore = E203,E501,W503,ANN101,ANN102 From 56e58ee568e083f3cf7e161aa846e710aacf1776 Mon Sep 17 00:00:00 2001 From: Shuyang Wu Date: Fri, 5 Nov 2021 19:24:45 -0400 Subject: [PATCH 577/649] Align Format of cn-zh Timeframe (#1058) --- arrow/locales.py | 4 ++-- tests/test_locales.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index b7b744c1a..f071f1366 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -951,7 +951,7 @@ class ChineseCNLocale(Locale): timeframes = { "now": "刚才", - "second": "一秒", + "second": "1秒", "seconds": "{0}秒", "minute": "1分钟", "minutes": "{0}分钟", @@ -959,7 +959,7 @@ class ChineseCNLocale(Locale): "hours": "{0}小时", "day": "1天", "days": "{0}天", - "week": "一周", + "week": "1周", "weeks": "{0}周", "month": "1个月", "months": "{0}个月", diff --git a/tests/test_locales.py b/tests/test_locales.py index 48c106133..653e197d5 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1839,7 +1839,7 @@ def test_format_timeframe(self): class TestChineseCNLocale: def test_format_timeframe(self): assert self.locale._format_timeframe("now", 0) == "刚才" - assert self.locale._format_timeframe("second", 1) == "一秒" + assert self.locale._format_timeframe("second", 1) == "1秒" assert self.locale._format_timeframe("seconds", 30) == "30秒" assert self.locale._format_timeframe("minute", 1) == "1分钟" assert self.locale._format_timeframe("minutes", 40) == "40分钟" @@ -1847,7 +1847,7 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("hours", 23) == "23小时" assert self.locale._format_timeframe("day", 1) == "1天" assert self.locale._format_timeframe("days", 12) == "12天" - assert self.locale._format_timeframe("week", 1) == "一周" + assert self.locale._format_timeframe("week", 1) == "1周" assert self.locale._format_timeframe("weeks", 38) == "38周" assert self.locale._format_timeframe("month", 1) == "1个月" assert self.locale._format_timeframe("months", 11) == "11个月" From d688de482c5379455dfad8163f56749e81b9e806 Mon Sep 17 00:00:00 2001 From: Soo Hur Date: Tue, 23 Nov 2021 19:29:44 +0100 Subject: [PATCH 578/649] Kazakh Locale (#1069) --- arrow/constants.py | 2 ++ arrow/locales.py | 68 +++++++++++++++++++++++++++++++++++++++++++ tests/test_arrow.py | 4 +++ tests/test_locales.py | 65 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+) diff --git a/arrow/constants.py b/arrow/constants.py index e41ffa5d1..008e8b7af 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -145,4 +145,6 @@ "ta-lk", "ur", "ur-pk", + "kk", + "kk-kz", } diff --git a/arrow/locales.py b/arrow/locales.py index f071f1366..2698074c5 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -5909,3 +5909,71 @@ class UrduLocale(Locale): "ہفتہ", "اتوار", ] + + +class KazakhLocale(Locale): + + names = ["kk", "kk-kz"] + + 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 = [ + "", + "Қаңтар", + "Ақпан", + "Наурыз", + "Сәуір", + "Мамыр", + "Маусым", + "Шілде", + "Тамыз", + "Қыркүйек", + "Қазан", + "Қараша", + "Желтоқсан", + ] + month_abbreviations = [ + "", + "Қан", + "Ақп", + "Нау", + "Сәу", + "Мам", + "Мау", + "Шіл", + "Там", + "Қыр", + "Қаз", + "Қар", + "Жел", + ] + + day_names = [ + "", + "Дүйсембі", + "Сейсенбі", + "Сәрсенбі", + "Бейсенбі", + "Жұма", + "Сенбі", + "Жексенбі", + ] + day_abbreviations = ["", "Дс", "Сс", "Ср", "Бс", "Жм", "Сб", "Жс"] diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 00d633cfa..7cb3b5104 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2449,6 +2449,8 @@ def locale_list_no_weeks() -> List[str]: "ta-lk", "ur", "ur-pk", + "kk", + "kk-kz", ] return tested_langs @@ -2511,6 +2513,8 @@ def locale_list_with_weeks() -> List[str]: "ta", "ta-in", "ta-lk", + "kk", + "kk-kz", ] return tested_langs diff --git a/tests/test_locales.py b/tests/test_locales.py index 653e197d5..74405eeff 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -2261,3 +2261,68 @@ 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 TestKazakhLocale: + def test_singles_mk(self): + 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("week", 1) == "бір апта" + assert self.locale._format_timeframe("month", 1) == "бір ай" + assert self.locale._format_timeframe("year", 1) == "бір жыл" + + def test_describe_mk(self): + assert self.locale.describe("second", only_distance=True) == "бір секунд" + assert self.locale.describe("second", only_distance=False) == "бір секунд кейін" + assert self.locale.describe("minute", only_distance=True) == "бір минут" + assert self.locale.describe("minute", only_distance=False) == "бір минут кейін" + assert self.locale.describe("hour", only_distance=True) == "бір сағат" + assert self.locale.describe("hour", only_distance=False) == "бір сағат кейін" + assert self.locale.describe("day", only_distance=True) == "бір күн" + assert self.locale.describe("day", only_distance=False) == "бір күн кейін" + assert self.locale.describe("week", only_distance=True) == "бір апта" + assert self.locale.describe("week", only_distance=False) == "бір апта кейін" + assert self.locale.describe("month", only_distance=True) == "бір ай" + assert self.locale.describe("month", only_distance=False) == "бір ай кейін" + assert self.locale.describe("year", only_distance=True) == "бір жыл" + assert self.locale.describe("year", only_distance=False) == "бір жыл кейін" + + def test_relative_mk(self): + assert self.locale._format_relative("қазір", "now", 0) == "қазір" + assert ( + self.locale._format_relative("1 секунд", "seconds", 1) == "1 секунд кейін" + ) + assert ( + self.locale._format_relative("1 секунд", "seconds", -1) == "1 секунд бұрын" + ) + assert self.locale._format_relative("1 минут", "minutes", 1) == "1 минут кейін" + assert self.locale._format_relative("1 минут", "minutes", -1) == "1 минут бұрын" + assert self.locale._format_relative("1 сағат", "hours", 1) == "1 сағат кейін" + assert self.locale._format_relative("1 сағат", "hours", -1) == "1 сағат бұрын" + assert self.locale._format_relative("1 күн", "days", 1) == "1 күн кейін" + assert self.locale._format_relative("1 күн", "days", -1) == "1 күн бұрын" + assert self.locale._format_relative("1 апта", "weeks", 1) == "1 апта кейін" + assert self.locale._format_relative("1 апта", "weeks", -1) == "1 апта бұрын" + assert self.locale._format_relative("1 ай", "months", 1) == "1 ай кейін" + assert self.locale._format_relative("1 ай", "months", -1) == "1 ай бұрын" + assert self.locale._format_relative("1 жыл", "years", 1) == "1 жыл кейін" + assert self.locale._format_relative("1 жыл", "years", -1) == "1 жыл бұрын" + + def test_plurals_mk(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("days", 12) == "12 күн" + assert self.locale._format_timeframe("week", 1) == "бір апта" + assert self.locale._format_timeframe("weeks", 38) == "38 апта" + 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 жыл" From be57df5a7474dda3f6c16176ad181fa176039f2c Mon Sep 17 00:00:00 2001 From: Neel Patel <53986205+neelrpatel@users.noreply.github.com> Date: Sun, 12 Dec 2021 18:27:43 -0500 Subject: [PATCH 579/649] Expand locale timeframe testing for multiple variations (#1073) --- tests/test_locales.py | 252 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 239 insertions(+), 13 deletions(-) diff --git a/tests/test_locales.py b/tests/test_locales.py index 74405eeff..54f99ef09 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -608,21 +608,71 @@ def test_format_relative_future(self): @pytest.mark.usefixtures("lang_locale") class TestCzechLocale: def test_format_timeframe(self): + # Now + assert self.locale._format_timeframe("now", 0) == "Teď" - assert self.locale._format_timeframe("hours", 2) == "2 hodiny" - assert self.locale._format_timeframe("hours", 5) == "5 hodin" + # Second(s) + assert self.locale._format_timeframe("second", -1) == "vteřina" + assert self.locale._format_timeframe("second", 0) == "vteřina" + assert self.locale._format_timeframe("second", 1) == "vteřina" + assert self.locale._format_timeframe("seconds", -2) == "2 sekundami" + assert self.locale._format_timeframe("seconds", -5) == "5 sekundami" + assert self.locale._format_timeframe("seconds", 2) == "2 sekundy" + assert self.locale._format_timeframe("seconds", 5) == "5 sekund" + + # Minute(s) + assert self.locale._format_timeframe("minute", -1) == "minutou" + assert self.locale._format_timeframe("minute", 0) == "0 minut" + assert self.locale._format_timeframe("minute", 1) == "minutu" + assert self.locale._format_timeframe("minutes", -2) == "2 minutami" + assert self.locale._format_timeframe("minutes", -5) == "5 minutami" + assert self.locale._format_timeframe("minutes", 2) == "2 minuty" + assert self.locale._format_timeframe("minutes", 5) == "5 minut" + + # Hour(s) + assert self.locale._format_timeframe("hour", -1) == "hodinou" assert self.locale._format_timeframe("hour", 0) == "0 hodin" + assert self.locale._format_timeframe("hour", 1) == "hodinu" assert self.locale._format_timeframe("hours", -2) == "2 hodinami" assert self.locale._format_timeframe("hours", -5) == "5 hodinami" - assert self.locale._format_timeframe("hour", 1) == "hodinu" - - assert self.locale._format_timeframe("now", 0) == "Teď" + assert self.locale._format_timeframe("hours", 2) == "2 hodiny" + assert self.locale._format_timeframe("hours", 5) == "5 hodin" - assert self.locale._format_timeframe("weeks", 2) == "2 týdny" - assert self.locale._format_timeframe("weeks", 5) == "5 týdnů" + # Day(s) + assert self.locale._format_timeframe("day", -1) == "dnem" + assert self.locale._format_timeframe("day", 0) == "0 dnů" + assert self.locale._format_timeframe("day", 1) == "den" + assert self.locale._format_timeframe("days", -2) == "2 dny" + assert self.locale._format_timeframe("days", -5) == "5 dny" + assert self.locale._format_timeframe("days", 2) == "2 dny" + assert self.locale._format_timeframe("days", 5) == "5 dnů" + + # Weeks(s) + assert self.locale._format_timeframe("week", -1) == "týdnem" assert self.locale._format_timeframe("week", 0) == "0 týdnů" + assert self.locale._format_timeframe("week", 1) == "týden" assert self.locale._format_timeframe("weeks", -2) == "2 týdny" assert self.locale._format_timeframe("weeks", -5) == "5 týdny" + assert self.locale._format_timeframe("weeks", 2) == "2 týdny" + assert self.locale._format_timeframe("weeks", 5) == "5 týdnů" + + # Month(s) + assert self.locale._format_timeframe("month", -1) == "měsícem" + assert self.locale._format_timeframe("month", 0) == "0 měsíců" + assert self.locale._format_timeframe("month", 1) == "měsíc" + assert self.locale._format_timeframe("months", -2) == "2 měsíci" + assert self.locale._format_timeframe("months", -5) == "5 měsíci" + assert self.locale._format_timeframe("months", 2) == "2 měsíce" + assert self.locale._format_timeframe("months", 5) == "5 měsíců" + + # Year(s) + assert self.locale._format_timeframe("year", -1) == "rokem" + assert self.locale._format_timeframe("year", 0) == "0 let" + assert self.locale._format_timeframe("year", 1) == "rok" + assert self.locale._format_timeframe("years", -2) == "2 lety" + assert self.locale._format_timeframe("years", -5) == "5 lety" + assert self.locale._format_timeframe("years", 2) == "2 roky" + assert self.locale._format_timeframe("years", 5) == "5 let" def test_format_relative_now(self): @@ -887,23 +937,46 @@ def test_multi_describe_mk(self): @pytest.mark.usefixtures("lang_locale") class TestHebrewLocale: def test_couple_of_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 שניות" + assert self.locale._format_timeframe("seconds", 10) == "10 שניות" + assert self.locale._format_timeframe("seconds", 11) == "11 שניות" + + # Minute(s) + assert self.locale._format_timeframe("minute", 1) == "דקה" + assert self.locale._format_timeframe("minutes", 2) == "2 דקות" + assert self.locale._format_timeframe("minutes", 10) == "10 דקות" + assert self.locale._format_timeframe("minutes", 11) == "11 דקות" + + # Day(s) assert self.locale._format_timeframe("day", 1) == "יום" assert self.locale._format_timeframe("days", 2) == "יומיים" assert self.locale._format_timeframe("days", 3) == "3 ימים" assert self.locale._format_timeframe("days", 80) == "80 יום" + # Hour(s) assert self.locale._format_timeframe("hour", 1) == "שעה" assert self.locale._format_timeframe("hours", 2) == "שעתיים" assert self.locale._format_timeframe("hours", 3) == "3 שעות" + assert self.locale._format_timeframe("hours", 11) == "11 שעות" + # Week(s) assert self.locale._format_timeframe("week", 1) == "שבוע" assert self.locale._format_timeframe("weeks", 2) == "שבועיים" assert self.locale._format_timeframe("weeks", 3) == "3 שבועות" + assert self.locale._format_timeframe("weeks", 11) == "11 שבועות" + # Month(s) assert self.locale._format_timeframe("month", 1) == "חודש" assert self.locale._format_timeframe("months", 2) == "חודשיים" assert self.locale._format_timeframe("months", 4) == "4 חודשים" + assert self.locale._format_timeframe("months", 11) == "11 חודשים" + # Year(s) assert self.locale._format_timeframe("year", 1) == "שנה" assert self.locale._format_timeframe("years", 2) == "שנתיים" assert self.locale._format_timeframe("years", 5) == "5 שנים" @@ -1033,13 +1106,44 @@ def test_ordinal_number(self): @pytest.mark.usefixtures("lang_locale") class TestFinnishLocale: def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", -2) == "2 tuntia" - assert self.locale._format_timeframe("hours", 2) == "2 tunnin" + # Now + assert self.locale._format_timeframe("now", 1) == "juuri nyt" + # 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" + + # Minute(s) + assert self.locale._format_timeframe("minute", -1) == "minuutti" + assert self.locale._format_timeframe("minute", 1) == "minuutin" + assert self.locale._format_timeframe("minutes", -2) == "2 minuuttia" + assert self.locale._format_timeframe("minutes", 2) == "2 minuutin" + + # Hour(s) assert self.locale._format_timeframe("hour", -1) == "tunti" assert self.locale._format_timeframe("hour", 1) == "tunnin" + assert self.locale._format_timeframe("hours", -2) == "2 tuntia" + assert self.locale._format_timeframe("hours", 2) == "2 tunnin" - assert self.locale._format_timeframe("now", 1) == "juuri nyt" + # 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("days", -2) == "2 päivää" + assert self.locale._format_timeframe("days", 2) == "2 päivän" + + # Month(s) + assert self.locale._format_timeframe("month", -1) == "kuukausi" + assert self.locale._format_timeframe("month", 1) == "kuukauden" + assert self.locale._format_timeframe("months", -2) == "2 kuukautta" + assert self.locale._format_timeframe("months", 2) == "2 kuukauden" + + # Year(s) + assert self.locale._format_timeframe("year", -1) == "vuosi" + assert self.locale._format_timeframe("year", 1) == "vuoden" + assert self.locale._format_timeframe("years", -2) == "2 vuotta" + assert self.locale._format_timeframe("years", 2) == "2 vuoden" def test_format_relative_now(self): result = self.locale._format_relative("juuri nyt", "now", 0) @@ -1085,11 +1189,45 @@ def test_weekday(self): @pytest.mark.usefixtures("lang_locale") class TestHungarianLocale: def test_format_timeframe(self): - assert self.locale._format_timeframe("hours", 2) == "2 óra" - assert self.locale._format_timeframe("hour", 0) == "egy órával" - assert self.locale._format_timeframe("hours", -2) == "2 órával" + # Now assert self.locale._format_timeframe("now", 0) == "éppen most" + # Second(s) + assert self.locale._format_timeframe("second", -1) == "egy második" + assert self.locale._format_timeframe("second", 1) == "egy második" + assert self.locale._format_timeframe("seconds", -2) == "2 másodpercekkel" + assert self.locale._format_timeframe("seconds", 2) == "2 pár másodperc" + + # Minute(s) + assert self.locale._format_timeframe("minute", -1) == "egy perccel" + assert self.locale._format_timeframe("minute", 1) == "egy perc" + assert self.locale._format_timeframe("minutes", -2) == "2 perccel" + assert self.locale._format_timeframe("minutes", 2) == "2 perc" + + # Hour(s) + assert self.locale._format_timeframe("hour", -1) == "egy órával" + assert self.locale._format_timeframe("hour", 1) == "egy óra" + assert self.locale._format_timeframe("hours", -2) == "2 órával" + assert self.locale._format_timeframe("hours", 2) == "2 óra" + + # Day(s) + assert self.locale._format_timeframe("day", -1) == "egy nappal" + assert self.locale._format_timeframe("day", 1) == "egy nap" + assert self.locale._format_timeframe("days", -2) == "2 nappal" + assert self.locale._format_timeframe("days", 2) == "2 nap" + + # Month(s) + assert self.locale._format_timeframe("month", -1) == "egy hónappal" + assert self.locale._format_timeframe("month", 1) == "egy hónap" + assert self.locale._format_timeframe("months", -2) == "2 hónappal" + assert self.locale._format_timeframe("months", 2) == "2 hónap" + + # Year(s) + assert self.locale._format_timeframe("year", -1) == "egy évvel" + assert self.locale._format_timeframe("year", 1) == "egy év" + assert self.locale._format_timeframe("years", -2) == "2 évvel" + assert self.locale._format_timeframe("years", 2) == "2 év" + @pytest.mark.usefixtures("lang_locale") class TestEsperantoLocale: @@ -1457,20 +1595,40 @@ def test_ordinal_number_tl(self): @pytest.mark.usefixtures("lang_locale") class TestCroatianLocale: def test_format_timeframe(self): + # Now assert self.locale._format_timeframe("now", 0) == "upravo sad" + + # Second(s) assert self.locale._format_timeframe("second", 1) == "sekundu" assert self.locale._format_timeframe("seconds", 3) == "3 sekunde" assert self.locale._format_timeframe("seconds", 30) == "30 sekundi" + + # Minute(s) assert self.locale._format_timeframe("minute", 1) == "minutu" assert self.locale._format_timeframe("minutes", 4) == "4 minute" assert self.locale._format_timeframe("minutes", 40) == "40 minuta" + + # Hour(s) assert self.locale._format_timeframe("hour", 1) == "sat" + assert self.locale._format_timeframe("hours", 4) == "4 sata" assert self.locale._format_timeframe("hours", 23) == "23 sati" + + # Day(s) assert self.locale._format_timeframe("day", 1) == "jedan dan" + assert self.locale._format_timeframe("days", 4) == "4 dana" assert self.locale._format_timeframe("days", 12) == "12 dana" + + # Week(s) + assert self.locale._format_timeframe("week", 1) == "tjedan" + assert self.locale._format_timeframe("weeks", 4) == "4 tjedna" + assert self.locale._format_timeframe("weeks", 12) == "12 tjedana" + + # Month(s) assert self.locale._format_timeframe("month", 1) == "mjesec" assert self.locale._format_timeframe("months", 2) == "2 mjeseca" assert self.locale._format_timeframe("months", 11) == "11 mjeseci" + + # Year(s) assert self.locale._format_timeframe("year", 1) == "godinu" assert self.locale._format_timeframe("years", 2) == "2 godine" assert self.locale._format_timeframe("years", 12) == "12 godina" @@ -1484,24 +1642,40 @@ def test_weekday(self): @pytest.mark.usefixtures("lang_locale") class TestSerbianLocale: def test_format_timeframe(self): + # Now assert self.locale._format_timeframe("now", 0) == "sada" + + # Second(s) assert self.locale._format_timeframe("second", 1) == "sekundu" assert self.locale._format_timeframe("seconds", 3) == "3 sekunde" assert self.locale._format_timeframe("seconds", 30) == "30 sekundi" + + # Minute(s) assert self.locale._format_timeframe("minute", 1) == "minutu" assert self.locale._format_timeframe("minutes", 4) == "4 minute" assert self.locale._format_timeframe("minutes", 40) == "40 minuta" + + # Hour(s) assert self.locale._format_timeframe("hour", 1) == "sat" assert self.locale._format_timeframe("hours", 3) == "3 sata" assert self.locale._format_timeframe("hours", 23) == "23 sati" + + # Day(s) assert self.locale._format_timeframe("day", 1) == "dan" + assert self.locale._format_timeframe("days", 4) == "4 dana" assert self.locale._format_timeframe("days", 12) == "12 dana" + + # Week(s) assert self.locale._format_timeframe("week", 1) == "nedelju" assert self.locale._format_timeframe("weeks", 2) == "2 nedelje" assert self.locale._format_timeframe("weeks", 11) == "11 nedelja" + + # Month(s) assert self.locale._format_timeframe("month", 1) == "mesec" assert self.locale._format_timeframe("months", 2) == "2 meseca" assert self.locale._format_timeframe("months", 11) == "11 meseci" + + # Year(s) assert self.locale._format_timeframe("year", 1) == "godinu" assert self.locale._format_timeframe("years", 2) == "2 godine" assert self.locale._format_timeframe("years", 12) == "12 godina" @@ -1607,20 +1781,50 @@ def test_weekday(self): @pytest.mark.usefixtures("lang_locale") class TestZuluLocale: def test_format_timeframe(self): + # Now assert self.locale._format_timeframe("now", 0) == "manje" + + # Second(s) assert self.locale._format_timeframe("second", -1) == "umzuzwana" assert self.locale._format_timeframe("second", 1) == "ngomzuzwana" assert self.locale._format_timeframe("seconds", -3) == "3 imizuzwana" + assert self.locale._format_timeframe("seconds", 3) == "3 ngemizuzwana" + + # Minute(s) assert self.locale._format_timeframe("minute", -1) == "umzuzu" + assert self.locale._format_timeframe("minute", 1) == "ngomzuzu" assert self.locale._format_timeframe("minutes", -4) == "4 imizuzu" + assert self.locale._format_timeframe("minutes", 4) == "4 ngemizuzu" + + # Hour(s) assert self.locale._format_timeframe("hour", -1) == "ihora" + assert self.locale._format_timeframe("hour", 1) == "ngehora" assert self.locale._format_timeframe("hours", -23) == "23 amahora" + assert self.locale._format_timeframe("hours", 23) == "23 emahoreni" + + # Day(s) assert self.locale._format_timeframe("day", -1) == "usuku" + assert self.locale._format_timeframe("day", 1) == "ngosuku" assert self.locale._format_timeframe("days", -12) == "12 izinsuku" + assert self.locale._format_timeframe("days", 12) == "12 ezinsukwini" + + # Day(s) + assert self.locale._format_timeframe("week", -1) == "isonto" + assert self.locale._format_timeframe("week", 1) == "ngesonto" + assert self.locale._format_timeframe("weeks", -12) == "12 amasonto" + assert self.locale._format_timeframe("weeks", 12) == "12 emasontweni" + + # Month(s) assert self.locale._format_timeframe("month", -1) == "inyanga" + assert self.locale._format_timeframe("month", 1) == "ngenyanga" assert self.locale._format_timeframe("months", -2) == "2 izinyanga" + assert self.locale._format_timeframe("months", 2) == "2 ezinyangeni" + + # Year(s) assert self.locale._format_timeframe("year", -1) == "unyaka" + assert self.locale._format_timeframe("year", 1) == "ngonyak" assert self.locale._format_timeframe("years", -2) == "2 iminyaka" + assert self.locale._format_timeframe("years", 2) == "2 eminyakeni" def test_weekday(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) @@ -2192,28 +2396,50 @@ def test_weekday(self): @pytest.mark.usefixtures("lang_locale") class TestSinhalaLocale: 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("second", 1) == "තත්පරයකින්" assert self.locale._format_timeframe("seconds", -30) == "තත්පර 30 ක" + 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 කින්" + # 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 කින්" + # Week(s) assert self.locale._format_timeframe("week", -1) == "සතියක" + assert self.locale._format_timeframe("week", 1) == "සතියකින්" assert self.locale._format_timeframe("weeks", -10) == "සති 10 ක" + assert self.locale._format_timeframe("weeks", 10) == "සති 10 කින්" + # 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", -21) == "අවුරුදු 21 ක" + assert self.locale._format_timeframe("years", 21) == "අවුරුදු 21 තුළ" def test_describe_si(self): assert self.locale.describe("second", only_distance=True) == "තත්පරයක්" From a2ebb7e20b4897ee7b7bf4676a5ac3e6c0f27f47 Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Sat, 18 Dec 2021 14:18:51 -0500 Subject: [PATCH 580/649] Dehumanize Support for Slavic Locales (#1077) --- arrow/arrow.py | 2 +- arrow/constants.py | 13 +++++++++ arrow/locales.py | 11 ++++++- tests/test_arrow.py | 71 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index fef66c103..d7504456e 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"{search_string}") + pattern = re.compile(fr"(^|\b|\d){search_string}") match = pattern.search(input_string) # If there is no match continue to next iteration diff --git a/arrow/constants.py b/arrow/constants.py index 008e8b7af..d26bc0d87 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -73,7 +73,20 @@ "zh-hk", "nl", "nl-nl", + "be", + "be-by", + "pl", + "pl-pl", + "ru", + "ru-ru", "af", + "bg", + "bg-bg", + "ua", + "uk", + "uk-ua", + "mk", + "mk-mk", "de", "de-de", "de-ch", diff --git a/arrow/locales.py b/arrow/locales.py index 2698074c5..6b1627e7e 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -172,7 +172,16 @@ def describe_multi( humanized = " ".join(parts) if not only_distance: - humanized = self._format_relative(humanized, *timeframes[-1]) + # Needed to determine the correct relative string to use + timeframe_value = 0 + + for _unit_name, unit_value in timeframes: + if trunc(unit_value) != 0: + timeframe_value = trunc(unit_value) + break + + # Note it doesn't matter the timeframe unit we use on the call, only the value + humanized = self._format_relative(humanized, "seconds", timeframe_value) return humanized diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 7cb3b5104..a2f08813c 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2373,7 +2373,20 @@ def locale_list_no_weeks() -> List[str]: "zh-hk", "nl", "nl-nl", + "be", + "be-by", + "pl", + "pl-pl", + "ru", + "ru-ru", "af", + "bg", + "bg-bg", + "ua", + "uk", + "uk-ua", + "mk", + "mk-mk", "de", "de-de", "de-ch", @@ -2485,6 +2498,12 @@ def locale_list_with_weeks() -> List[str]: "zh-hk", "nl", "nl-nl", + "pl", + "pl-pl", + "ru", + "ru-ru", + "mk", + "mk-mk", "de", "de-de", "de-ch", @@ -2520,6 +2539,27 @@ def locale_list_with_weeks() -> List[str]: return tested_langs +@pytest.fixture(scope="class") +def slavic_locales() -> List[str]: + tested_langs = [ + "be", + "be-by", + "pl", + "pl-pl", + "ru", + "ru-ru", + "bg", + "bg-bg", + "ua", + "uk", + "uk-ua", + "mk", + "mk-mk", + ] + + return tested_langs + + class TestArrowDehumanize: def test_now(self, locale_list_no_weeks: List[str]): @@ -2883,6 +2923,37 @@ def test_no_units_modified(self, locale_list_no_weeks: List[str]): with pytest.raises(ValueError): 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, + 1, + 2, + 5, + 21, + 22, + 25, + ] + + # Only need to test on seconds as logic holds for all slavic plural units + for lang in slavic_locales: + for unit in units: + arw = arrow.Arrow(2000, 2, 18, 1, 50, 30) + + past = arw.shift(minutes=-1 * unit, days=-1) + future = arw.shift(minutes=unit, days=1) + + past_string = past.humanize( + arw, locale=lang, granularity=["minute", "day"] + ) + future_string = future.humanize( + arw, locale=lang, granularity=["minute", "day"] + ) + + assert arw.dehumanize(past_string, locale=lang) == past + assert arw.dehumanize(future_string, locale=lang) == future + class TestArrowIsBetween: def test_start_before_end(self): From 74bc0a1d2583ca28994bd8dca7929ea5b061c25f Mon Sep 17 00:00:00 2001 From: Anish Nyayachavadi <55898433+anishnya@users.noreply.github.com> Date: Mon, 27 Dec 2021 16:07:58 -0500 Subject: [PATCH 581/649] Dehumanize Support for Czech and Slovak Locales (#1079) --- arrow/constants.py | 4 ++++ arrow/locales.py | 42 ++++++++++++++++++++++++++++-------------- tests/test_arrow.py | 36 ++++++++++++++++++++++++++++++++++++ tests/test_locales.py | 28 ++++++++++++++-------------- 4 files changed, 82 insertions(+), 28 deletions(-) diff --git a/arrow/constants.py b/arrow/constants.py index d26bc0d87..1189d07c7 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -110,6 +110,10 @@ "da-dk", "ml", "hi", + "cs", + "cs-cz", + "sk", + "sk-sk", "fa", "fa-ir", "mr", diff --git a/arrow/locales.py b/arrow/locales.py index 6b1627e7e..ddbecb77e 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -3067,44 +3067,51 @@ class CzechLocale(Locale): timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "Teď", - "second": {"past": "vteřina", "future": "vteřina", "zero": "vteřina"}, + "second": {"past": "vteřina", "future": "vteřina"}, "seconds": { + "zero": "vteřina", "past": "{0} sekundami", "future-singular": "{0} sekundy", "future-paucal": "{0} sekund", }, - "minute": {"past": "minutou", "future": "minutu", "zero": "{0} minut"}, + "minute": {"past": "minutou", "future": "minutu"}, "minutes": { + "zero": "{0} minut", "past": "{0} minutami", "future-singular": "{0} minuty", "future-paucal": "{0} minut", }, - "hour": {"past": "hodinou", "future": "hodinu", "zero": "{0} hodin"}, + "hour": {"past": "hodinou", "future": "hodinu"}, "hours": { + "zero": "{0} hodin", "past": "{0} hodinami", "future-singular": "{0} hodiny", "future-paucal": "{0} hodin", }, - "day": {"past": "dnem", "future": "den", "zero": "{0} dnů"}, + "day": {"past": "dnem", "future": "den"}, "days": { + "zero": "{0} dnů", "past": "{0} dny", "future-singular": "{0} dny", "future-paucal": "{0} dnů", }, - "week": {"past": "týdnem", "future": "týden", "zero": "{0} týdnů"}, + "week": {"past": "týdnem", "future": "týden"}, "weeks": { + "zero": "{0} týdnů", "past": "{0} týdny", "future-singular": "{0} týdny", "future-paucal": "{0} týdnů", }, - "month": {"past": "měsícem", "future": "měsíc", "zero": "{0} měsíců"}, + "month": {"past": "měsícem", "future": "měsíc"}, "months": { + "zero": "{0} měsíců", "past": "{0} měsíci", "future-singular": "{0} měsíce", "future-paucal": "{0} měsíců", }, - "year": {"past": "rokem", "future": "rok", "zero": "{0} let"}, + "year": {"past": "rokem", "future": "rok"}, "years": { + "zero": "{0} let", "past": "{0} lety", "future-singular": "{0} roky", "future-paucal": "{0} let", @@ -3190,44 +3197,51 @@ class SlovakLocale(Locale): timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = { "now": "Teraz", - "second": {"past": "sekundou", "future": "sekundu", "zero": "{0} sekúnd"}, + "second": {"past": "sekundou", "future": "sekundu"}, "seconds": { + "zero": "{0} sekúnd", "past": "{0} sekundami", "future-singular": "{0} sekundy", "future-paucal": "{0} sekúnd", }, - "minute": {"past": "minútou", "future": "minútu", "zero": "{0} minút"}, + "minute": {"past": "minútou", "future": "minútu"}, "minutes": { + "zero": "{0} minút", "past": "{0} minútami", "future-singular": "{0} minúty", "future-paucal": "{0} minút", }, - "hour": {"past": "hodinou", "future": "hodinu", "zero": "{0} hodín"}, + "hour": {"past": "hodinou", "future": "hodinu"}, "hours": { + "zero": "{0} hodín", "past": "{0} hodinami", "future-singular": "{0} hodiny", "future-paucal": "{0} hodín", }, - "day": {"past": "dňom", "future": "deň", "zero": "{0} dní"}, + "day": {"past": "dňom", "future": "deň"}, "days": { + "zero": "{0} dní", "past": "{0} dňami", "future-singular": "{0} dni", "future-paucal": "{0} dní", }, - "week": {"past": "týždňom", "future": "týždeň", "zero": "{0} týždňov"}, + "week": {"past": "týždňom", "future": "týždeň"}, "weeks": { + "zero": "{0} týždňov", "past": "{0} týždňami", "future-singular": "{0} týždne", "future-paucal": "{0} týždňov", }, - "month": {"past": "mesiacom", "future": "mesiac", "zero": "{0} mesiacov"}, + "month": {"past": "mesiacom", "future": "mesiac"}, "months": { + "zero": "{0} mesiacov", "past": "{0} mesiacmi", "future-singular": "{0} mesiace", "future-paucal": "{0} mesiacov", }, - "year": {"past": "rokom", "future": "rok", "zero": "{0} rokov"}, + "year": {"past": "rokom", "future": "rok"}, "years": { + "zero": "{0} rokov", "past": "{0} rokmi", "future-singular": "{0} roky", "future-paucal": "{0} rokov", diff --git a/tests/test_arrow.py b/tests/test_arrow.py index a2f08813c..60f3a64b3 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2410,6 +2410,10 @@ def locale_list_no_weeks() -> List[str]: "da-dk", "ml", "hi", + "cs", + "cs-cz", + "sk", + "sk-sk", "fa", "fa-ir", "mr", @@ -2511,6 +2515,10 @@ def locale_list_with_weeks() -> List[str]: "pt", "pt-pt", "pt-br", + "cs", + "cs-cz", + "sk", + "sk-sk", "tl", "tl-ph", "vi", @@ -2954,6 +2962,34 @@ def test_slavic_locales(self, slavic_locales: List[str]): assert arw.dehumanize(past_string, locale=lang) == past assert arw.dehumanize(future_string, locale=lang) == future + def test_czech_slovak(self): + + # Relevant units for Slavic locale plural logic + units = [ + 0, + 1, + 2, + 5, + ] + + # Only need to test on seconds as logic holds for all slavic plural units + for lang in ["cs"]: + for unit in units: + arw = arrow.Arrow(2000, 2, 18, 1, 50, 30) + + past = arw.shift(minutes=-1 * unit, days=-1) + future = arw.shift(minutes=unit, days=1) + + past_string = past.humanize( + arw, locale=lang, granularity=["minute", "day"] + ) + future_string = future.humanize( + arw, locale=lang, granularity=["minute", "day"] + ) + + assert arw.dehumanize(past_string, locale=lang) == past + assert arw.dehumanize(future_string, locale=lang) == future + class TestArrowIsBetween: def test_start_before_end(self): diff --git a/tests/test_locales.py b/tests/test_locales.py index 54f99ef09..54536c0a0 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -613,8 +613,8 @@ def test_format_timeframe(self): # Second(s) assert self.locale._format_timeframe("second", -1) == "vteřina" - assert self.locale._format_timeframe("second", 0) == "vteřina" assert self.locale._format_timeframe("second", 1) == "vteřina" + assert self.locale._format_timeframe("seconds", 0) == "vteřina" assert self.locale._format_timeframe("seconds", -2) == "2 sekundami" assert self.locale._format_timeframe("seconds", -5) == "5 sekundami" assert self.locale._format_timeframe("seconds", 2) == "2 sekundy" @@ -622,8 +622,8 @@ def test_format_timeframe(self): # Minute(s) assert self.locale._format_timeframe("minute", -1) == "minutou" - assert self.locale._format_timeframe("minute", 0) == "0 minut" assert self.locale._format_timeframe("minute", 1) == "minutu" + assert self.locale._format_timeframe("minutes", 0) == "0 minut" assert self.locale._format_timeframe("minutes", -2) == "2 minutami" assert self.locale._format_timeframe("minutes", -5) == "5 minutami" assert self.locale._format_timeframe("minutes", 2) == "2 minuty" @@ -631,8 +631,8 @@ def test_format_timeframe(self): # Hour(s) assert self.locale._format_timeframe("hour", -1) == "hodinou" - assert self.locale._format_timeframe("hour", 0) == "0 hodin" assert self.locale._format_timeframe("hour", 1) == "hodinu" + assert self.locale._format_timeframe("hours", 0) == "0 hodin" assert self.locale._format_timeframe("hours", -2) == "2 hodinami" assert self.locale._format_timeframe("hours", -5) == "5 hodinami" assert self.locale._format_timeframe("hours", 2) == "2 hodiny" @@ -640,8 +640,8 @@ def test_format_timeframe(self): # Day(s) assert self.locale._format_timeframe("day", -1) == "dnem" - assert self.locale._format_timeframe("day", 0) == "0 dnů" assert self.locale._format_timeframe("day", 1) == "den" + assert self.locale._format_timeframe("days", 0) == "0 dnů" assert self.locale._format_timeframe("days", -2) == "2 dny" assert self.locale._format_timeframe("days", -5) == "5 dny" assert self.locale._format_timeframe("days", 2) == "2 dny" @@ -649,8 +649,8 @@ def test_format_timeframe(self): # Weeks(s) assert self.locale._format_timeframe("week", -1) == "týdnem" - assert self.locale._format_timeframe("week", 0) == "0 týdnů" assert self.locale._format_timeframe("week", 1) == "týden" + assert self.locale._format_timeframe("weeks", 0) == "0 týdnů" assert self.locale._format_timeframe("weeks", -2) == "2 týdny" assert self.locale._format_timeframe("weeks", -5) == "5 týdny" assert self.locale._format_timeframe("weeks", 2) == "2 týdny" @@ -658,8 +658,8 @@ def test_format_timeframe(self): # Month(s) assert self.locale._format_timeframe("month", -1) == "měsícem" - assert self.locale._format_timeframe("month", 0) == "0 měsíců" assert self.locale._format_timeframe("month", 1) == "měsíc" + assert self.locale._format_timeframe("months", 0) == "0 měsíců" assert self.locale._format_timeframe("months", -2) == "2 měsíci" assert self.locale._format_timeframe("months", -5) == "5 měsíci" assert self.locale._format_timeframe("months", 2) == "2 měsíce" @@ -667,8 +667,8 @@ def test_format_timeframe(self): # Year(s) assert self.locale._format_timeframe("year", -1) == "rokem" - assert self.locale._format_timeframe("year", 0) == "0 let" assert self.locale._format_timeframe("year", 1) == "rok" + assert self.locale._format_timeframe("years", 0) == "0 let" assert self.locale._format_timeframe("years", -2) == "2 lety" assert self.locale._format_timeframe("years", -5) == "5 lety" assert self.locale._format_timeframe("years", 2) == "2 roky" @@ -697,7 +697,7 @@ 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" - assert self.locale._format_timeframe("second", 0) == "0 sekúnd" + assert self.locale._format_timeframe("seconds", 0) == "0 sekúnd" assert self.locale._format_timeframe("second", 1) == "sekundu" assert self.locale._format_timeframe("seconds", 2) == "2 sekundy" assert self.locale._format_timeframe("seconds", 5) == "5 sekúnd" @@ -705,7 +705,7 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("minutes", -5) == "5 minútami" assert self.locale._format_timeframe("minutes", -2) == "2 minútami" assert self.locale._format_timeframe("minute", -1) == "minútou" - assert self.locale._format_timeframe("minute", 0) == "0 minút" + assert self.locale._format_timeframe("minutes", 0) == "0 minút" assert self.locale._format_timeframe("minute", 1) == "minútu" assert self.locale._format_timeframe("minutes", 2) == "2 minúty" assert self.locale._format_timeframe("minutes", 5) == "5 minút" @@ -713,7 +713,7 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("hours", -5) == "5 hodinami" assert self.locale._format_timeframe("hours", -2) == "2 hodinami" assert self.locale._format_timeframe("hour", -1) == "hodinou" - assert self.locale._format_timeframe("hour", 0) == "0 hodín" + assert self.locale._format_timeframe("hours", 0) == "0 hodín" assert self.locale._format_timeframe("hour", 1) == "hodinu" assert self.locale._format_timeframe("hours", 2) == "2 hodiny" assert self.locale._format_timeframe("hours", 5) == "5 hodín" @@ -721,7 +721,7 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("days", -5) == "5 dňami" assert self.locale._format_timeframe("days", -2) == "2 dňami" assert self.locale._format_timeframe("day", -1) == "dňom" - assert self.locale._format_timeframe("day", 0) == "0 dní" + assert self.locale._format_timeframe("days", 0) == "0 dní" assert self.locale._format_timeframe("day", 1) == "deň" assert self.locale._format_timeframe("days", 2) == "2 dni" assert self.locale._format_timeframe("days", 5) == "5 dní" @@ -729,7 +729,7 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("weeks", -5) == "5 týždňami" assert self.locale._format_timeframe("weeks", -2) == "2 týždňami" assert self.locale._format_timeframe("week", -1) == "týždňom" - assert self.locale._format_timeframe("week", 0) == "0 týždňov" + assert self.locale._format_timeframe("weeks", 0) == "0 týždňov" assert self.locale._format_timeframe("week", 1) == "týždeň" assert self.locale._format_timeframe("weeks", 2) == "2 týždne" assert self.locale._format_timeframe("weeks", 5) == "5 týždňov" @@ -737,7 +737,7 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("months", -5) == "5 mesiacmi" assert self.locale._format_timeframe("months", -2) == "2 mesiacmi" assert self.locale._format_timeframe("month", -1) == "mesiacom" - assert self.locale._format_timeframe("month", 0) == "0 mesiacov" + assert self.locale._format_timeframe("months", 0) == "0 mesiacov" assert self.locale._format_timeframe("month", 1) == "mesiac" assert self.locale._format_timeframe("months", 2) == "2 mesiace" assert self.locale._format_timeframe("months", 5) == "5 mesiacov" @@ -745,7 +745,7 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("years", -5) == "5 rokmi" assert self.locale._format_timeframe("years", -2) == "2 rokmi" assert self.locale._format_timeframe("year", -1) == "rokom" - assert self.locale._format_timeframe("year", 0) == "0 rokov" + assert self.locale._format_timeframe("years", 0) == "0 rokov" assert self.locale._format_timeframe("year", 1) == "rok" assert self.locale._format_timeframe("years", 2) == "2 roky" assert self.locale._format_timeframe("years", 5) == "5 rokov" From 774ac01303d9b6d5f0bb321b6965af10ac050ff8 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Fri, 31 Dec 2021 18:14:02 -0500 Subject: [PATCH 582/649] Break up requirements.txt into multiple requirements files (#1080) * Break up requirements.txt into multiple requirements files. * Fix linting --- .github/workflows/continuous_integration.yml | 72 ++++--------------- .pre-commit-config.yaml | 24 ++++--- Makefile | 15 ++-- arrow/locales.py | 4 +- requirements/requirements-docs.txt | 5 ++ .../requirements-tests.txt | 11 ++- requirements/requirements.txt | 2 + tox.ini | 15 ++-- 8 files changed, 59 insertions(+), 89 deletions(-) create mode 100644 requirements/requirements-docs.txt rename requirements-dev.txt => requirements/requirements-tests.txt (51%) create mode 100644 requirements/requirements.txt diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 140bf4466..d60e4bd3b 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -12,7 +12,6 @@ jobs: test: name: ${{ matrix.os }} (${{ matrix.python-version }}) runs-on: ${{ matrix.os }} - strategy: fail-fast: false matrix: @@ -22,56 +21,31 @@ jobs: # pypy3 randomly fails on Windows builds - os: windows-latest python-version: "pypy-3.7" - + include: + - os: ubuntu-latest + path: ~/.cache/pip + - os: macos-latest + path: ~/Library/Caches/pip + - os: windows-latest + path: ~\AppData\Local\pip\Cache steps: - # Check out latest code - uses: actions/checkout@v2 - - # Configure pip cache - - name: Cache pip (Linux) - uses: actions/cache@v2 - if: startsWith(runner.os, 'Linux') - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Cache pip (macOS) - uses: actions/cache@v2 - if: startsWith(runner.os, 'macOS') - with: - path: ~/Library/Caches/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Cache pip (Windows) + - name: Cache pip uses: actions/cache@v2 - if: startsWith(runner.os, 'Windows') with: - path: ~\AppData\Local\pip\Cache - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - # Set up Python + 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 with: python-version: ${{ matrix.python-version }} - - # Install dependencies - name: Install dependencies run: | pip install -U pip setuptools wheel pip install -U tox tox-gh-actions - - # Run tests - name: Test with tox run: tox - - # Upload coverage report - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 with: @@ -79,45 +53,29 @@ jobs: lint: runs-on: ubuntu-latest - steps: - # Check out latest code - uses: actions/checkout@v2 - - # Set up Python - name: Set up Python 3.10 uses: actions/setup-python@v2 with: python-version: "3.10" - - # Configure pip cache - name: Cache pip uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - # Configure pre-commit cache + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} + restore-keys: ${{ runner.os }}-pip- - name: Cache pre-commit uses: actions/cache@v2 with: path: ~/.cache/pre-commit key: ${{ runner.os }}-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} - restore-keys: | - ${{ runner.os }}-pre-commit- - - # Install dependencies + restore-keys: ${{ runner.os }}-pre-commit- - name: Install dependencies run: | pip install -U pip setuptools wheel pip install -U tox - - # Lint code - name: Lint code - run: tox -e lint - - # Lint docs + run: tox -e lint -- --show-diff-on-failure - name: Lint docs run: tox -e docs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c65b26187..28b5f8a00 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,25 +2,27 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: fix-encoding-pragma - args: [--remove] - - id: requirements-txt-fixer - id: check-ast - id: check-yaml - id: check-case-conflict - id: check-docstring-first - id: check-merge-conflict + - 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.9.3 + rev: 5.10.1 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.29.0 + rev: v2.30.1 hooks: - id: pyupgrade args: [--py36-plus] @@ -29,12 +31,14 @@ repos: hooks: - id: python-no-eval - id: python-check-blanket-noqa + - id: python-check-mock-methods - id: python-use-type-annotations - id: rst-backticks - id: rst-directive-colons - id: rst-inline-touching-normal + - id: text-unicode-replacement-char - repo: https://github.com/psf/black - rev: 21.9b0 + rev: 21.12b0 hooks: - id: black args: [--safe, --quiet, --target-version=py36] @@ -44,7 +48,7 @@ repos: - id: flake8 additional_dependencies: [flake8-bugbear,flake8-annotations] - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.910-1' + rev: 'v0.930' hooks: - id: mypy additional_dependencies: [types-python-dateutil] diff --git a/Makefile b/Makefile index c473e3f13..5f8851579 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: auto test docs clean -auto: build39 +auto: build310 build36: PYTHON_VER = python3.6 build37: PYTHON_VER = python3.7 @@ -12,7 +12,8 @@ build36 build37 build38 build39 build310: clean $(PYTHON_VER) -m venv venv . venv/bin/activate; \ pip install -U pip setuptools wheel; \ - pip install -r requirements-dev.txt; \ + pip install -r requirements/requirements-tests.txt; \ + pip install -r requirements/requirements-docs.txt; \ pre-commit install test: @@ -22,14 +23,20 @@ test: lint: . venv/bin/activate; \ - pre-commit run --all-files --show-diff-on-failure + pre-commit run --all-files -docs: +clean-docs: rm -rf docs/_build + +docs: . venv/bin/activate; \ cd docs; \ make html +live-docs: clean-docs + . venv/bin/activate; \ + sphinx-autobuild docs docs/_build/html + clean: clean-dist rm -rf venv .pytest_cache ./**/__pycache__ rm -f .coverage coverage.xml ./**/*.pyc diff --git a/arrow/locales.py b/arrow/locales.py index ddbecb77e..d50446087 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -46,7 +46,7 @@ ] -_locale_map: Dict[str, Type["Locale"]] = dict() +_locale_map: Dict[str, Type["Locale"]] = {} def get_locale(name: str) -> "Locale": @@ -5743,7 +5743,7 @@ class SinhalaLocale(Locale): } # Sinhala: the general format to describe timeframe is different from past and future, # so we do not copy the original timeframes dictionary - timeframes_only_distance = dict() + timeframes_only_distance = {} timeframes_only_distance["second"] = "තත්පරයක්" timeframes_only_distance["seconds"] = "තත්පර {0}" timeframes_only_distance["minute"] = "මිනිත්තුවක්" diff --git a/requirements/requirements-docs.txt b/requirements/requirements-docs.txt new file mode 100644 index 000000000..de59f1a3a --- /dev/null +++ b/requirements/requirements-docs.txt @@ -0,0 +1,5 @@ +-r requirements.txt +doc8 +sphinx +sphinx-autobuild +sphinx-autodoc-typehints diff --git a/requirements-dev.txt b/requirements/requirements-tests.txt similarity index 51% rename from requirements-dev.txt rename to requirements/requirements-tests.txt index 75f443410..7e9fbe3f9 100644 --- a/requirements-dev.txt +++ b/requirements/requirements-tests.txt @@ -1,11 +1,10 @@ +-r requirements.txt dateparser==1.* -pre-commit==2.* -pytest==6.* -pytest-cov==3.* -pytest-mock==3.* +pre-commit +pytest +pytest-cov +pytest-mock python-dateutil>=2.7.0 pytz==2021.1 simplejson==3.* -sphinx==4.* -sphinx-autodoc-typehints==1.* typing_extensions; python_version < '3.8' diff --git a/requirements/requirements.txt b/requirements/requirements.txt new file mode 100644 index 000000000..bcdff0e86 --- /dev/null +++ b/requirements/requirements.txt @@ -0,0 +1,2 @@ +python-dateutil>=2.7.0 +typing_extensions; python_version < '3.8' diff --git a/tox.ini b/tox.ini index b77461760..fefa3e7e4 100644 --- a/tox.ini +++ b/tox.ini @@ -13,27 +13,22 @@ python = 3.10: py310 [testenv] -deps = -rrequirements-dev.txt +deps = -r requirements/requirements-tests.txt allowlist_externals = pytest commands = pytest [testenv:lint] -basepython = python3 skip_install = true deps = pre-commit -commands = - pre-commit install - pre-commit run --all-files --show-diff-on-failure +commands_pre = pre-commit install +commands = pre-commit run --all-files {posargs} [testenv:docs] -basepython = python3 skip_install = true changedir = docs deps = - doc8 - sphinx - sphinx-autodoc-typehints - python-dateutil + -r requirements/requirements-tests.txt + -r requirements/requirements-docs.txt allowlist_externals = make commands = doc8 index.rst ../README.rst --extension .rst --ignore D001 From 022845e639993dd9ccbcee01014ae7b5ea0671a6 Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Fri, 31 Dec 2021 18:28:43 -0500 Subject: [PATCH 583/649] Generate doc PDFs with xelatex, which has better support for unicode characters. --- docs/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 907d78c0c..f106cb7f2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,3 +58,7 @@ html_sidebars = { "**": ["about.html", "localtoc.html", "relations.html", "searchbox.html"] } + +# Generate PDFs with unicode characters +# https://docs.readthedocs.io/en/stable/guides/pdf-non-ascii-languages.html +latex_engine = "xelatex" From fecbada0e4cc06b52f08a9e1151045b9e056ab3d Mon Sep 17 00:00:00 2001 From: Jad Chaar Date: Fri, 31 Dec 2021 22:05:28 -0500 Subject: [PATCH 584/649] Fix requirements files in MANIFEST.in --- MANIFEST.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 8ac191e0e..9abe97738 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ -include LICENSE CHANGELOG.rst README.rst Makefile requirements-dev.txt tox.ini +include LICENSE CHANGELOG.rst README.rst Makefile tox.ini +recursive-include requirements *.txt recursive-include tests *.py recursive-include docs *.py *.rst *.bat Makefile From 7d7926a056f820743352b9ffbb0043f49505ec2f Mon Sep 17 00:00:00 2001 From: kaiyang-code <57576013+kaiyang-code@users.noreply.github.com> Date: Sat, 1 Jan 2022 22:11:05 -0500 Subject: [PATCH 585/649] Expand Hong Kong, ChineseCN and ChineseTW Locale Test Cases (#1076) Co-authored-by: Anish Nyayachavadi --- tests/test_locales.py | 150 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/tests/test_locales.py b/tests/test_locales.py index 54536c0a0..cb2f60ff7 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -2018,6 +2018,56 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("year", 1) == "1年" assert self.locale._format_timeframe("years", 12) == "12年" + assert self.locale._format_timeframe("second", -1) == "1秒" + assert self.locale._format_timeframe("seconds", -30) == "30秒" + assert self.locale._format_timeframe("minute", -1) == "1分鐘" + assert self.locale._format_timeframe("minutes", -40) == "40分鐘" + assert self.locale._format_timeframe("hour", -1) == "1小時" + assert self.locale._format_timeframe("hours", -23) == "23小時" + assert self.locale._format_timeframe("day", -1) == "1天" + assert self.locale._format_timeframe("days", -12) == "12天" + assert self.locale._format_timeframe("week", -1) == "1星期" + assert self.locale._format_timeframe("weeks", -38) == "38星期" + assert self.locale._format_timeframe("month", -1) == "1個月" + assert self.locale._format_timeframe("months", -11) == "11個月" + assert self.locale._format_timeframe("year", -1) == "1年" + assert self.locale._format_timeframe("years", -12) == "12年" + + def test_format_relative_now(self): + assert self.locale._format_relative("剛才", "now", 0) == "剛才" + + def test_format_relative_past(self): + assert self.locale._format_relative("1秒", "second", 1) == "1秒後" + assert self.locale._format_relative("2秒", "seconds", 2) == "2秒後" + assert self.locale._format_relative("1分鐘", "minute", 1) == "1分鐘後" + assert self.locale._format_relative("2分鐘", "minutes", 2) == "2分鐘後" + assert self.locale._format_relative("1小時", "hour", 1) == "1小時後" + assert self.locale._format_relative("2小時", "hours", 2) == "2小時後" + assert self.locale._format_relative("1天", "day", 1) == "1天後" + assert self.locale._format_relative("2天", "days", 2) == "2天後" + assert self.locale._format_relative("1星期", "week", 1) == "1星期後" + assert self.locale._format_relative("2星期", "weeks", 2) == "2星期後" + assert self.locale._format_relative("1個月", "month", 1) == "1個月後" + assert self.locale._format_relative("2個月", "months", 2) == "2個月後" + assert self.locale._format_relative("1年", "year", 1) == "1年後" + assert self.locale._format_relative("2年", "years", 2) == "2年後" + + def test_format_relative_future(self): + assert self.locale._format_relative("1秒", "second", -1) == "1秒前" + assert self.locale._format_relative("2秒", "seconds", -2) == "2秒前" + assert self.locale._format_relative("1分鐘", "minute", -1) == "1分鐘前" + assert self.locale._format_relative("2分鐘", "minutes", -2) == "2分鐘前" + assert self.locale._format_relative("1小時", "hour", -1) == "1小時前" + assert self.locale._format_relative("2小時", "hours", -2) == "2小時前" + assert self.locale._format_relative("1天", "day", -1) == "1天前" + assert self.locale._format_relative("2天", "days", -2) == "2天前" + assert self.locale._format_relative("1星期", "week", -1) == "1星期前" + assert self.locale._format_relative("2星期", "weeks", -2) == "2星期前" + assert self.locale._format_relative("1個月", "month", -1) == "1個月前" + assert self.locale._format_relative("2個月", "months", -2) == "2個月前" + assert self.locale._format_relative("1年", "year", -1) == "1年前" + assert self.locale._format_relative("2年", "years", -2) == "2年前" + @pytest.mark.usefixtures("lang_locale") class TestChineseTWLocale: @@ -2038,6 +2088,56 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("year", 1) == "1年" assert self.locale._format_timeframe("years", 12) == "12年" + assert self.locale._format_timeframe("second", -1) == "1秒" + assert self.locale._format_timeframe("seconds", -30) == "30秒" + assert self.locale._format_timeframe("minute", -1) == "1分鐘" + assert self.locale._format_timeframe("minutes", -40) == "40分鐘" + assert self.locale._format_timeframe("hour", -1) == "1小時" + assert self.locale._format_timeframe("hours", -23) == "23小時" + assert self.locale._format_timeframe("day", -1) == "1天" + assert self.locale._format_timeframe("days", -12) == "12天" + assert self.locale._format_timeframe("week", -1) == "1週" + assert self.locale._format_timeframe("weeks", -38) == "38週" + assert self.locale._format_timeframe("month", -1) == "1個月" + assert self.locale._format_timeframe("months", -11) == "11個月" + assert self.locale._format_timeframe("year", -1) == "1年" + assert self.locale._format_timeframe("years", -12) == "12年" + + def test_format_relative_now(self): + assert self.locale._format_relative("剛才", "now", 0) == "剛才" + + def test_format_relative_past(self): + assert self.locale._format_relative("1秒", "second", 1) == "1秒後" + assert self.locale._format_relative("2秒", "seconds", 2) == "2秒後" + assert self.locale._format_relative("1分鐘", "minute", 1) == "1分鐘後" + assert self.locale._format_relative("2分鐘", "minutes", 2) == "2分鐘後" + assert self.locale._format_relative("1小時", "hour", 1) == "1小時後" + assert self.locale._format_relative("2小時", "hours", 2) == "2小時後" + assert self.locale._format_relative("1天", "day", 1) == "1天後" + assert self.locale._format_relative("2天", "days", 2) == "2天後" + assert self.locale._format_relative("1週", "week", 1) == "1週後" + assert self.locale._format_relative("2週", "weeks", 2) == "2週後" + assert self.locale._format_relative("1個月", "month", 1) == "1個月後" + assert self.locale._format_relative("2個月", "months", 2) == "2個月後" + assert self.locale._format_relative("1年", "year", 1) == "1年後" + assert self.locale._format_relative("2年", "years", 2) == "2年後" + + def test_format_relative_future(self): + assert self.locale._format_relative("1秒", "second", -1) == "1秒前" + assert self.locale._format_relative("2秒", "seconds", -2) == "2秒前" + assert self.locale._format_relative("1分鐘", "minute", -1) == "1分鐘前" + assert self.locale._format_relative("2分鐘", "minutes", -2) == "2分鐘前" + assert self.locale._format_relative("1小時", "hour", -1) == "1小時前" + assert self.locale._format_relative("2小時", "hours", -2) == "2小時前" + assert self.locale._format_relative("1天", "day", -1) == "1天前" + assert self.locale._format_relative("2天", "days", -2) == "2天前" + assert self.locale._format_relative("1週", "week", -1) == "1週前" + assert self.locale._format_relative("2週", "weeks", -2) == "2週前" + assert self.locale._format_relative("1個月", "month", -1) == "1個月前" + assert self.locale._format_relative("2個月", "months", -2) == "2個月前" + assert self.locale._format_relative("1年", "year", -1) == "1年前" + assert self.locale._format_relative("2年", "years", -2) == "2年前" + @pytest.mark.usefixtures("lang_locale") class TestChineseCNLocale: @@ -2058,6 +2158,56 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("year", 1) == "1年" assert self.locale._format_timeframe("years", 12) == "12年" + assert self.locale._format_timeframe("second", -1) == "1秒" + assert self.locale._format_timeframe("seconds", -30) == "30秒" + assert self.locale._format_timeframe("minute", -1) == "1分钟" + assert self.locale._format_timeframe("minutes", -40) == "40分钟" + assert self.locale._format_timeframe("hour", -1) == "1小时" + assert self.locale._format_timeframe("hours", -23) == "23小时" + assert self.locale._format_timeframe("day", -1) == "1天" + assert self.locale._format_timeframe("days", -12) == "12天" + assert self.locale._format_timeframe("week", -1) == "1周" + assert self.locale._format_timeframe("weeks", -38) == "38周" + assert self.locale._format_timeframe("month", -1) == "1个月" + assert self.locale._format_timeframe("months", -11) == "11个月" + assert self.locale._format_timeframe("year", -1) == "1年" + assert self.locale._format_timeframe("years", -12) == "12年" + + def test_format_relative_now(self): + assert self.locale._format_relative("刚才", "now", 0) == "刚才" + + def test_format_relative_past(self): + assert self.locale._format_relative("1秒", "second", 1) == "1秒后" + assert self.locale._format_relative("2秒", "seconds", 2) == "2秒后" + assert self.locale._format_relative("1分钟", "minute", 1) == "1分钟后" + assert self.locale._format_relative("2分钟", "minutes", 2) == "2分钟后" + assert self.locale._format_relative("1小时", "hour", 1) == "1小时后" + assert self.locale._format_relative("2小时", "hours", 2) == "2小时后" + assert self.locale._format_relative("1天", "day", 1) == "1天后" + assert self.locale._format_relative("2天", "days", 2) == "2天后" + assert self.locale._format_relative("1周", "week", 1) == "1周后" + assert self.locale._format_relative("2周", "weeks", 2) == "2周后" + assert self.locale._format_relative("1个月", "month", 1) == "1个月后" + assert self.locale._format_relative("2个月", "months", 2) == "2个月后" + assert self.locale._format_relative("1年", "year", 1) == "1年后" + assert self.locale._format_relative("2年", "years", 2) == "2年后" + + def test_format_relative_future(self): + assert self.locale._format_relative("1秒", "second", -1) == "1秒前" + assert self.locale._format_relative("2秒", "seconds", -2) == "2秒前" + assert self.locale._format_relative("1分钟", "minute", -1) == "1分钟前" + assert self.locale._format_relative("2分钟", "minutes", -2) == "2分钟前" + assert self.locale._format_relative("1小时", "hour", -1) == "1小时前" + assert self.locale._format_relative("2小时", "hours", -2) == "2小时前" + assert self.locale._format_relative("1天", "day", -1) == "1天前" + assert self.locale._format_relative("2天", "days", -2) == "2天前" + assert self.locale._format_relative("1周", "week", -1) == "1周前" + assert self.locale._format_relative("2周", "weeks", -2) == "2周前" + assert self.locale._format_relative("1个月", "month", -1) == "1个月前" + assert self.locale._format_relative("2个月", "months", -2) == "2个月前" + assert self.locale._format_relative("1年", "year", -1) == "1年前" + assert self.locale._format_relative("2年", "years", -2) == "2年前" + @pytest.mark.usefixtures("lang_locale") class TestSwahiliLocale: From e43524088f78efacb425524445a886600660d854 Mon Sep 17 00:00:00 2001 From: Stian Jensen Date: Wed, 5 Jan 2022 22:29:06 +0100 Subject: [PATCH 586/649] Fix ordinals for norwegian languages (#1074) In Norwegian, there should be a period after the day of the month. This also adds basic test coverage to both norwegian languages, and fixes some small errors uncovered by those tests. --- arrow/locales.py | 12 ++++- tests/test_locales.py | 112 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/arrow/locales.py b/arrow/locales.py index d50446087..b7fb0d324 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2056,6 +2056,8 @@ class NorwegianLocale(Locale): "hours": "{0} timer", "day": "en dag", "days": "{0} dager", + "week": "en uke", + "weeks": "{0} uker", "month": "en måned", "months": "{0} måneder", "year": "ett år", @@ -2105,6 +2107,9 @@ class NorwegianLocale(Locale): ] day_abbreviations = ["", "ma", "ti", "on", "to", "fr", "lø", "sø"] + def _ordinal_number(self, n: int) -> str: + return f"{n}." + class NewNorwegianLocale(Locale): @@ -2123,7 +2128,9 @@ class NewNorwegianLocale(Locale): "hours": "{0} timar", "day": "ein dag", "days": "{0} dagar", - "month": "en månad", + "week": "ei veke", + "weeks": "{0} veker", + "month": "ein månad", "months": "{0} månader", "year": "eitt år", "years": "{0} år", @@ -2172,6 +2179,9 @@ class NewNorwegianLocale(Locale): ] day_abbreviations = ["", "må", "ty", "on", "to", "fr", "la", "su"] + def _ordinal_number(self, n: int) -> str: + return f"{n}." + class PortugueseLocale(Locale): names = ["pt", "pt-pt"] diff --git a/tests/test_locales.py b/tests/test_locales.py index cb2f60ff7..5e8799144 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -2702,3 +2702,115 @@ def test_plurals_mk(self): assert self.locale._format_timeframe("months", 11) == "11 ай" assert self.locale._format_timeframe("year", 1) == "бір жыл" assert self.locale._format_timeframe("years", 12) == "12 жыл" + + +@pytest.mark.usefixtures("lang_locale") +class TestNorwegianLocale: + def test_describe(self): + assert self.locale.describe("now", only_distance=True) == "nå nettopp" + assert self.locale.describe("now", only_distance=False) == "nå nettopp" + + def test_plurals(self): + assert self.locale._format_timeframe("now", 0) == "nå nettopp" + assert self.locale._format_timeframe("second", 1) == "ett sekund" + assert self.locale._format_timeframe("seconds", 30) == "30 sekunder" + assert self.locale._format_timeframe("minute", 1) == "ett minutt" + assert self.locale._format_timeframe("minutes", 40) == "40 minutter" + assert self.locale._format_timeframe("hour", 1) == "en time" + assert self.locale._format_timeframe("hours", 23) == "23 timer" + assert self.locale._format_timeframe("day", 1) == "en dag" + assert self.locale._format_timeframe("days", 12) == "12 dager" + assert self.locale._format_timeframe("week", 1) == "en uke" + assert self.locale._format_timeframe("weeks", 38) == "38 uker" + assert self.locale._format_timeframe("month", 1) == "en måned" + assert self.locale._format_timeframe("months", 11) == "11 måneder" + assert self.locale._format_timeframe("year", 1) == "ett år" + assert self.locale._format_timeframe("years", 12) == "12 år" + + def test_ordinal_number(self): + assert self.locale.ordinal_number(0) == "0." + 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" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "lørdag" + assert self.locale.day_abbreviation(dt.isoweekday()) == "lø" + + +@pytest.mark.usefixtures("lang_locale") +class TestNewNorwegianLocale: + def test_describe(self): + assert self.locale.describe("now", only_distance=True) == "no nettopp" + assert self.locale.describe("now", only_distance=False) == "no nettopp" + + def test_plurals(self): + assert self.locale._format_timeframe("now", 0) == "no nettopp" + assert self.locale._format_timeframe("second", 1) == "eitt sekund" + assert self.locale._format_timeframe("seconds", 30) == "30 sekund" + assert self.locale._format_timeframe("minute", 1) == "eitt minutt" + assert self.locale._format_timeframe("minutes", 40) == "40 minutt" + assert self.locale._format_timeframe("hour", 1) == "ein time" + assert self.locale._format_timeframe("hours", 23) == "23 timar" + assert self.locale._format_timeframe("day", 1) == "ein dag" + assert self.locale._format_timeframe("days", 12) == "12 dagar" + assert self.locale._format_timeframe("week", 1) == "ei veke" + assert self.locale._format_timeframe("weeks", 38) == "38 veker" + assert self.locale._format_timeframe("month", 1) == "ein månad" + assert self.locale._format_timeframe("months", 11) == "11 månader" + assert self.locale._format_timeframe("year", 1) == "eitt år" + assert self.locale._format_timeframe("years", 12) == "12 år" + + def test_ordinal_number(self): + assert self.locale.ordinal_number(0) == "0." + 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" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "laurdag" + assert self.locale.day_abbreviation(dt.isoweekday()) == "la" From 4c4689c6d97ed2b1f37a67b96c561266c66ee088 Mon Sep 17 00:00:00 2001 From: Gaganpreet Date: Wed, 19 Jan 2022 22:00:01 +0100 Subject: [PATCH 587/649] Fix outdated Python 2 print in docs (#1086) --- arrow/arrow.py | 2 +- docs/index.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index d7504456e..21b0347fc 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -759,7 +759,7 @@ def interval( >>> start = datetime(2013, 5, 5, 12, 30) >>> end = datetime(2013, 5, 5, 17, 15) >>> for r in arrow.Arrow.interval('hour', start, end, 2): - ... print r + ... print(r) ... (, ) (, ) diff --git a/docs/index.rst b/docs/index.rst index 43895b044..d4f9ec2a7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -315,7 +315,7 @@ You can also get a range of time spans: >>> 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 + ... print(r) ... (, ) (, ) @@ -330,7 +330,7 @@ Or just iterate over a range of time: >>> 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) + ... print(repr(r)) ... From f8a65212df1ab234013a8a116436b3532c82b05d Mon Sep 17 00:00:00 2001 From: Chris <30196510+systemcatch@users.noreply.github.com> Date: Thu, 20 Jan 2022 13:20:21 +0000 Subject: [PATCH 588/649] Bump version to 1.2.2 and update CHANGELOG (#1085) --- CHANGELOG.rst | 11 +++++++++++ arrow/_version.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 87a1ef2bc..12bc86a65 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,17 @@ Changelog ========= +1.2.2 (2022-01-19) +------------------ + +- [NEW] Added Kazakh locale. +- [FIX] The Belarusian, Bulgarian, Czech, Macedonian, Polish, Russian, Slovak and Ukrainian locales now support ``dehumanize``. +- [FIX] Minor bug fixes and improvements to ChineseCN, Indonesian, Norwegian, and Russian locales. +- [FIX] Expanded testing for multiple locales. +- [INTERNAL] Started using ``xelatex`` for pdf generation in documentation. +- [INTERNAL] Split requirements file into ``requirements.txt``, ``requirements-docs.txt`` and ``requirements-tests.txt``. +- [INTERNAL] Added ``flake8-annotations`` package for type linting in ``pre-commit``. + 1.2.1 (2021-10-24) ------------------ diff --git a/arrow/_version.py b/arrow/_version.py index a955fdae1..bc86c944f 100644 --- a/arrow/_version.py +++ b/arrow/_version.py @@ -1 +1 @@ -__version__ = "1.2.1" +__version__ = "1.2.2" From cdc8d337835ab4132e0a3f3de1f73581eaa42e81 Mon Sep 17 00:00:00 2001 From: fjerhammer Date: Mon, 14 Feb 2022 05:04:52 +0100 Subject: [PATCH 589/649] Fix Issues and Add Tests for Danish Language (#1091) --- arrow/locales.py | 9 ++++++-- tests/test_locales.py | 52 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index b7fb0d324..3c1a3bb9a 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -2866,19 +2866,21 @@ class DanishLocale(Locale): names = ["da", "da-dk"] past = "for {0} siden" - future = "efter {0}" + future = "om {0}" and_word = "og" timeframes = { "now": "lige nu", "second": "et sekund", - "seconds": "{0} et par sekunder", + "seconds": "{0} sekunder", "minute": "et minut", "minutes": "{0} minutter", "hour": "en time", "hours": "{0} timer", "day": "en dag", "days": "{0} dage", + "week": "en uge", + "weeks": "{0} uger", "month": "en måned", "months": "{0} måneder", "year": "et år", @@ -2928,6 +2930,9 @@ class DanishLocale(Locale): ] day_abbreviations = ["", "man", "tir", "ons", "tor", "fre", "lør", "søn"] + def _ordinal_number(self, n: int) -> str: + return f"{n}." + class MalayalamLocale(Locale): diff --git a/tests/test_locales.py b/tests/test_locales.py index 5e8799144..a9da0bc06 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -2814,3 +2814,55 @@ def test_weekday(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) assert self.locale.day_name(dt.isoweekday()) == "laurdag" assert self.locale.day_abbreviation(dt.isoweekday()) == "la" + + +@pytest.mark.usefixtures("lang_locale") +class TestDanishLocale: + def test_describe(self): + assert self.locale.describe("now", only_distance=True) == "lige nu" + assert self.locale.describe("now", only_distance=False) == "lige nu" + + def test_plurals(self): + assert self.locale._format_timeframe("now", 0) == "lige nu" + assert self.locale._format_timeframe("second", 1) == "et sekund" + assert self.locale._format_timeframe("seconds", 30) == "30 sekunder" + assert self.locale._format_timeframe("minute", 1) == "et minut" + assert self.locale._format_timeframe("minutes", 40) == "40 minutter" + assert self.locale._format_timeframe("hour", 1) == "en time" + assert self.locale._format_timeframe("hours", 23) == "23 timer" + assert self.locale._format_timeframe("day", 1) == "en dag" + assert self.locale._format_timeframe("days", 12) == "12 dage" + assert self.locale._format_timeframe("week", 1) == "en uge" + assert self.locale._format_timeframe("weeks", 38) == "38 uger" + assert self.locale._format_timeframe("month", 1) == "en måned" + assert self.locale._format_timeframe("months", 11) == "11 måneder" + assert self.locale._format_timeframe("year", 1) == "et år" + assert self.locale._format_timeframe("years", 12) == "12 år" + + def test_ordinal_number(self): + assert self.locale.ordinal_number(0) == "0." + 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("lige nu", "now", 0) + + assert result == "lige nu" + + 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" + + def test_weekday(self): + dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) + assert self.locale.day_name(dt.isoweekday()) == "lørdag" + assert self.locale.day_abbreviation(dt.isoweekday()) == "lør" From 5ad47f1cf58fe4b2f053e37be04362096441716b Mon Sep 17 00:00:00 2001 From: cyriaka90 Date: Mon, 14 Feb 2022 14:20:49 +0100 Subject: [PATCH 590/649] Add Georgian Locale (#1088) --- arrow/constants.py | 2 + arrow/locales.py | 85 +++++++++++++++++++++++++++++++++++++++++++ tests/test_arrow.py | 2 + tests/test_locales.py | 53 +++++++++++++++++++++++++++ 4 files changed, 142 insertions(+) diff --git a/arrow/constants.py b/arrow/constants.py index 1189d07c7..926650434 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -162,6 +162,8 @@ "ta-lk", "ur", "ur-pk", + "ka", + "ka-ge", "kk", "kk-kz", } diff --git a/arrow/locales.py b/arrow/locales.py index 3c1a3bb9a..4164ebdf3 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -5704,6 +5704,91 @@ class AlbanianLocale(Locale): ] +class GeorgianLocale(Locale): + + names = ["ka", "ka-ge"] + + past = "{0} წინ" # ts’in + future = "{0} შემდეგ" # shemdeg + and_word = "და" # da + + timeframes = { + "now": "ახლა", # akhla + # When a cardinal qualifies a noun, it stands in the singular + "second": "წამის", # ts’amis + "seconds": "{0} წამის", + "minute": "წუთის", # ts’utis + "minutes": "{0} წუთის", + "hour": "საათის", # saatis + "hours": "{0} საათის", + "day": "დღის", # dghis + "days": "{0} დღის", + "week": "კვირის", # k’viris + "weeks": "{0} კვირის", + "month": "თვის", # tvis + "months": "{0} თვის", + "year": "წლის", # ts’lis + "years": "{0} წლის", + } + + month_names = [ + # modern month names + "", + "იანვარი", # Ianvari + "თებერვალი", # Tebervali + "მარტი", # Mart'i + "აპრილი", # Ap'rili + "მაისი", # Maisi + "ივნისი", # Ivnisi + "ივლისი", # Ivlisi + "აგვისტო", # Agvist'o + "სექტემბერი", # Sekt'emberi + "ოქტომბერი", # Okt'omberi + "ნოემბერი", # Noemberi + "დეკემბერი", # Dek'emberi + ] + + month_abbreviations = [ + # no abbr. found yet + "", + "იანვარი", # Ianvari + "თებერვალი", # Tebervali + "მარტი", # Mart'i + "აპრილი", # Ap'rili + "მაისი", # Maisi + "ივნისი", # Ivnisi + "ივლისი", # Ivlisi + "აგვისტო", # Agvist'o + "სექტემბერი", # Sekt'emberi + "ოქტომბერი", # Okt'omberi + "ნოემბერი", # Noemberi + "დეკემბერი", # Dek'emberi + ] + + day_names = [ + "", + "ორშაბათი", # orshabati + "სამშაბათი", # samshabati + "ოთხშაბათი", # otkhshabati + "ხუთშაბათი", # khutshabati + "პარასკევი", # p’arask’evi + "შაბათი", # shabati + # "k’vira" also serves as week; to avoid confusion "k’vira-dge" can be used for Sunday + "კვირა", # k’vira + ] + + day_abbreviations = [ + "", + "ორშაბათი", # orshabati + "სამშაბათი", # samshabati + "ოთხშაბათი", # otkhshabati + "ხუთშაბათი", # khutshabati + "პარასკევი", # p’arask’evi + "შაბათი", # shabati + "კვირა", # k’vira + ] + + class SinhalaLocale(Locale): names = ["si", "si-lk"] diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 60f3a64b3..2e2ffe916 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2466,6 +2466,8 @@ def locale_list_no_weeks() -> List[str]: "ta-lk", "ur", "ur-pk", + "ka", + "ka-ge", "kk", "kk-kz", ] diff --git a/tests/test_locales.py b/tests/test_locales.py index a9da0bc06..ba2300bd4 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1161,6 +1161,59 @@ def test_ordinal_number(self): assert self.locale.ordinal_number(1) == "1." +@pytest.mark.usefixtures("lang_locale") +class TestGeorgianLocale: + 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("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 დღის" + + # Day(s) + assert self.locale._format_timeframe("week", -1) == "კვირის" + assert self.locale._format_timeframe("week", 1) == "კვირის" + assert self.locale._format_timeframe("weeks", -12) == "12 კვირის" + assert self.locale._format_timeframe("weeks", 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()) == "შაბათი" + + @pytest.mark.usefixtures("lang_locale") class TestGermanLocale: def test_ordinal_number(self): From c9cae9e8c25f021bc1b0075ad7eba25d10463308 Mon Sep 17 00:00:00 2001 From: Ching-Yi <82449718+ChingYi-AX@users.noreply.github.com> Date: Fri, 18 Feb 2022 06:13:46 +0100 Subject: [PATCH 591/649] Add Amharic Locale (#1093) --- arrow/constants.py | 2 + arrow/locales.py | 171 ++++++++++++++++++++++++++++++++++++++++++ tests/test_locales.py | 76 +++++++++++++++++++ 3 files changed, 249 insertions(+) diff --git a/arrow/constants.py b/arrow/constants.py index 926650434..4c6fa5cb5 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -166,4 +166,6 @@ "ka-ge", "kk", "kk-kz", + "am", + "am-et", } diff --git a/arrow/locales.py b/arrow/locales.py index 4164ebdf3..d5652370f 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -6100,3 +6100,174 @@ class KazakhLocale(Locale): "Жексенбі", ] day_abbreviations = ["", "Дс", "Сс", "Ср", "Бс", "Жм", "Сб", "Жс"] + + +class AmharicLocale(Locale): + names = ["am", "am-et"] + + past = "{0} በፊት" + future = "{0} ውስጥ" + and_word = "እና" + + timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[Mapping[str, str], str]]] = { + "now": "አሁን", + "second": { + "past": "ከአንድ ሰከንድ", + "future": "በአንድ ሰከንድ", + }, + "seconds": { + "past": "ከ {0} ሰከንድ", + "future": "በ {0} ሰከንድ", + }, + "minute": { + "past": "ከአንድ ደቂቃ", + "future": "በአንድ ደቂቃ", + }, + "minutes": { + "past": "ከ {0} ደቂቃዎች", + "future": "በ {0} ደቂቃዎች", + }, + "hour": { + "past": "ከአንድ ሰዓት", + "future": "በአንድ ሰዓት", + }, + "hours": { + "past": "ከ {0} ሰዓታት", + "future": "በ {0} ሰከንድ", + }, + "day": { + "past": "ከአንድ ቀን", + "future": "በአንድ ቀን", + }, + "days": { + "past": "ከ {0} ቀናት", + "future": "በ {0} ቀናት", + }, + "week": { + "past": "ከአንድ ሳምንት", + "future": "በአንድ ሳምንት", + }, + "weeks": { + "past": "ከ {0} ሳምንታት", + "future": "በ {0} ሳምንታት", + }, + "month": { + "past": "ከአንድ ወር", + "future": "በአንድ ወር", + }, + "months": { + "past": "ከ {0} ወር", + "future": "በ {0} ወራት", + }, + "year": { + "past": "ከአንድ አመት", + "future": "በአንድ አመት", + }, + "years": { + "past": "ከ {0} ዓመታት", + "future": "በ {0} ዓመታት", + }, + } + # Amharic: the general format to describe timeframe is different from past and future, + # so we do not copy the original timeframes dictionary + timeframes_only_distance = { + "second": "አንድ ሰከንድ", + "seconds": "{0} ሰከንድ", + "minute": "አንድ ደቂቃ", + "minutes": "{0} ደቂቃዎች", + "hour": "አንድ ሰዓት", + "hours": "{0} ሰዓት", + "day": "አንድ ቀን", + "days": "{0} ቀናት", + "week": "አንድ ሳምንት", + "weeks": "{0} ሳምንት", + "month": "አንድ ወር", + "months": "{0} ወራት", + "year": "አንድ አመት", + "years": "{0} ዓመታት", + } + + month_names = [ + "", + "ጃንዩወሪ", + "ፌብሩወሪ", + "ማርች", + "ኤፕሪል", + "ሜይ", + "ጁን", + "ጁላይ", + "ኦገስት", + "ሴፕቴምበር", + "ኦክቶበር", + "ኖቬምበር", + "ዲሴምበር", + ] + + month_abbreviations = [ + "", + "ጃንዩ", + "ፌብሩ", + "ማርች", + "ኤፕሪ", + "ሜይ", + "ጁን", + "ጁላይ", + "ኦገስ", + "ሴፕቴ", + "ኦክቶ", + "ኖቬም", + "ዲሴም", + ] + + day_names = [ + "", + "ሰኞ", + "ማክሰኞ", + "ረቡዕ", + "ሐሙስ", + "ዓርብ", + "ቅዳሜ", + "እሑድ", + ] + day_abbreviations = ["", "እ", "ሰ", "ማ", "ረ", "ሐ", "ዓ", "ቅ"] + + def _ordinal_number(self, n: int) -> str: + return f"{n}ኛ" + + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + """ + Amharic awares time frame format function, takes into account + the differences between general, past, and future forms (three different suffixes). + """ + abs_delta = abs(delta) + form = self.timeframes[timeframe] + + if isinstance(form, str): + return form.format(abs_delta) + + if delta > 0: + key = "future" + else: + key = "past" + form = form[key] + + return form.format(abs_delta) + + def describe( + self, + timeframe: TimeFrameLiteral, + delta: Union[float, int] = 1, # key is always future when only_distance=False + only_distance: bool = False, + ) -> str: + """Describes a delta within a timeframe in plain language. + + :param timeframe: a string representing a timeframe. + :param delta: a quantity representing a delta in a timeframe. + :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords + """ + + if not only_distance: + return super().describe(timeframe, delta, only_distance) + humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) + + return humanized diff --git a/tests/test_locales.py b/tests/test_locales.py index ba2300bd4..0e42074d6 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -2919,3 +2919,79 @@ def test_weekday(self): dt = arrow.Arrow(2015, 4, 11, 17, 30, 00) assert self.locale.day_name(dt.isoweekday()) == "lørdag" assert self.locale.day_abbreviation(dt.isoweekday()) == "lør" + + +@pytest.mark.usefixtures("lang_locale") +class TestAmharicLocale: + def test_format_timeframe(self): + assert self.locale._format_timeframe("now", 0) == "አሁን" + # second(s) + assert self.locale._format_timeframe("second", 1) == "በአንድ ሰከንድ" + assert self.locale._format_timeframe("second", -1) == "ከአንድ ሰከንድ" + assert self.locale._format_timeframe("seconds", 6) == "በ 6 ሰከንድ" + assert self.locale._format_timeframe("seconds", -36) == "ከ 36 ሰከንድ" + # 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 ደቂቃዎች" + # hour(s) + assert self.locale._format_timeframe("hour", 1) == "በአንድ ሰዓት" + assert self.locale._format_timeframe("hour", -1) == "ከአንድ ሰዓት" + assert self.locale._format_timeframe("hours", 7) == "በ 7 ሰከንድ" + assert self.locale._format_timeframe("hours", -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_describe_am(self): + assert self.locale.describe("second", only_distance=True) == "አንድ ሰከንድ" + assert ( + self.locale.describe("second", only_distance=False) == "በአንድ ሰከንድ ውስጥ" + ) # (in) a second + + assert self.locale.describe("minute", only_distance=True) == "አንድ ደቂቃ" + assert ( + self.locale.describe("minute", only_distance=False) == "በአንድ ደቂቃ ውስጥ" + ) # (in) a minute + + def test_format_relative_now(self): + result = self.locale._format_relative("አሁን", "now", 0) + assert result == "አሁን" + + 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 + + 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 80af201e58221e2106eb4264cf45c16282ed5270 Mon Sep 17 00:00:00 2001 From: Elaheh Salehi Rizi Date: Wed, 2 Mar 2022 14:39:26 +0100 Subject: [PATCH 592/649] 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 4c6fa5cb5..83627cd38 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 d5652370f..37791c8bb 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 2e2ffe916..742f41d4b 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 0e42074d6..3ad3a2a7a 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 593/649] 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 3ad3a2a7a..98f55ddd2 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 594/649] 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 98f55ddd2..37c857e35 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 595/649] 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 37c857e35..3ea584b34 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 596/649] 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 28b5f8a00..b352b70b7 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 21b0347fc..1ede107f5 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 4c6fa5cb5..ce29bf1a3 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 2e2ffe916..863b3cf13 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 53bba20d3..f368126c6 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 4a4cfe41a..bb4ab1480 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 fefa3e7e4..c51432a2c 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 597/649] 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 ce29bf1a3..b06f7b13e 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 d5652370f..f6723741c 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 863b3cf13..3809f1281 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 0e42074d6..ed62dc804 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 598/649] 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 b06f7b13e..c0fef8cdb 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 f6723741c..282ed0dcd 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 3809f1281..2289c5832 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 ed62dc804..2927a492e 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 599/649] 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 a94ddea77..c25b98a24 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 600/649] 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 c25b98a24..d0f314480 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 601/649] 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 d0f314480..3627497f5 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 4df9667c8..5ff00c1be 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 602/649] 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 f106cb7f2..68800bca2 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 603/649] 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 12bc86a65..3bf23e021 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 bc86c944f..10aa336ce 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 604/649] 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 d60e4bd3b..1ab9b5d1a 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 605/649] 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 3627497f5..ef7a8edd9 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 5ff00c1be..099f6f671 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 606/649] 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 ef7a8edd9..f0d4bc196 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 099f6f671..bef91d745 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 607/649] 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 e95d78b0d..6bf2fba24 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 bb4ab1480..e92d30c1a 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 608/649] 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 f0d4bc196..ec6af7261 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 609/649] 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 1ab9b5d1a..1f9d5547f 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 350a5a0f8..52563cf9a 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 c51432a2c..fcd2d9c1f 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 610/649] 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 fcd2d9c1f..11d70cb26 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 611/649] 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 1f9d5547f..21b5417cb 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 b352b70b7..2847af8bc 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 612/649] 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 b352b70b7..457e509ef 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 ec6af7261..ea4c84b2a 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 bef91d745..8312a9390 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 613/649] 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 1ede107f5..2e1d977c0 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 6bf2fba24..ee31a5bc9 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 3add2419c..6ffbd02b0 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 614/649] 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 bef91d745..0a7f9137a 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 615/649] 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 3bf23e021..5b079798f 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 1ede107f5..9802a9104 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 38d5000a4..5cd12c827 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 e92d30c1a..bdcc10264 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 616/649] 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 21b5417cb..34d9c4f26 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 5f8851579..f55a3dce9 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 617/649] 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 000000000..3cf4d394a --- /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 68800bca2..dee714701 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 000000000..6ebd3346f --- /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 000000000..aef0e880b --- /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 d4f9ec2a7..0ad2fdba4 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 22e1e59c8..ed21b4879 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 de59f1a3a..abc47b28d 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 618/649] 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 d13b4529d..b035115ab 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 619/649] 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 34d9c4f26..29b8df6d8 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 b035115ab..b8a60a2c1 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 000000000..57f8dd088 --- /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 4f9eea5d1..ff864f3ba 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 f55a3dce9..27d5cbe47 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 e855eee0c..8d329efd8 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 aad4af8bd..f35085f1c 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 728bea1aa..d45f71539 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 ea4c84b2a..b2e7ee03f 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 ee31a5bc9..645e3da74 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 dee714701..aa6fb4440 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 abc47b28d..35ca4ded4 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 bcdff0e86..65134a19a 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 52563cf9a..a2d8921eb 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 5cd12c827..507c1ab0f 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 f368126c6..4e328000e 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 06831f1e0..0b6c256cf 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 4bbbd3dc2..d6abb2a11 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 bdcc10264..1932b450d 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 11d70cb26..03c4d2af3 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 620/649] 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 b2e7ee03f..fe8d43955 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 621/649] 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 b8a60a2c1..b99f4c48f 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 65134a19a..7d97ff8e7 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 6ffbd02b0..916477e58 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 a2d8921eb..d58b9f3bc 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 622/649] 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 fe8d43955..34b2a0986 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 d6abb2a11..cd14274e6 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 623/649] 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 000000000..35e5fb73d --- /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 9abe97738..000000000 --- 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 27d5cbe47..9db6fba06 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 000000000..5ccc497a1 --- /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 7e9fbe3f9..77005e0b5 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 d58b9f3bc..000000000 --- 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 03c4d2af3..c410a8e5e 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 624/649] 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 5b079798f..b5daf6ed4 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 10aa336ce..67bc602ab 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 625/649] 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 8d329efd8..6c617ae77 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 53d163b99..532e95969 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 d45f71539..8cd61e908 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 34b2a0986..cd00b035a 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 645e3da74..b794f1b7e 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 626/649] 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 6c617ae77..782852567 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 aef0e880b..5bdf337d4 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 627/649] 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 5ccc497a1..89609bd1d 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 77005e0b5..1f2a2ec4c 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 7d97ff8e7..65134a19a 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 628/649] #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 782852567..c3f208dfd 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 507c1ab0f..becdd53a2 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 4e328000e..0ee9c4e08 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 0b6c256cf..36e15d9fc 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 1932b450d..3eb44d165 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 3b32e1bc5..2454dac56 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 629/649] 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 cd00b035a..d29d9f45b 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 cd14274e6..3e0035b0b 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 630/649] 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 d29d9f45b..c1b7869b0 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 3e0035b0b..2122b9c7a 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 631/649] 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 000000000..5ace4600a --- /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 632/649] 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 29b8df6d8..f26cb6eb4 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 35e5fb73d..4eb0f225c 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 633/649] 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 f26cb6eb4..383b7dc49 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 4eb0f225c..0c5006d3d 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 634/649] 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 c3f208dfd..ee959d7d7 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 becdd53a2..daf6209f8 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 635/649] 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 383b7dc49..f9e96f018 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 0c5006d3d..84adac2fa 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 636/649] 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 f9e96f018..588bff253 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 637/649] =?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 c1b7869b0..7f91b5658 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 2122b9c7a..f58eb221e 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 638/649] 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 ee959d7d7..6e9c3cf71 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 639/649] 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 7f91b5658..94f7902d5 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 f58eb221e..3eff7f444 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 640/649] 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 588bff253..0f573850b 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 b99f4c48f..942ff760d 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 9db6fba06..b1b9f53bb 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 69f91fe5c..11c441558 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 6e9c3cf71..9d1f5e305 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 f35085f1c..53eb8d12a 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 94f7902d5..de05d8d5f 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 89609bd1d..68e0d6c09 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 c410a8e5e..51c2c6a6f 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 641/649] 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 de05d8d5f..6c10134d6 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 642/649] 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 6c10134d6..16abd6f67 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 643/649] 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 16abd6f67..6ed2120cb 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 3eff7f444..b1916ae06 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 644/649] 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 6ed2120cb..4cc31a4ab 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 b1916ae06..6db3ad261 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 645/649] 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 b794f1b7e..39297887d 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 646/649] 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 4cc31a4ab..63b2d48ce 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 647/649] 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 68e0d6c09..3e2369173 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 1f2a2ec4c..94e5cc84e 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 daf6209f8..5afe9baa7 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 36e15d9fc..b09b778f7 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 95b47c166..7a74b7e46 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 648/649] 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 0f573850b..c94ba640e 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 649/649] 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 bc5970970..9232b379c 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 8cd61e908..6634545f0 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 b09b778f7..538682885 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)