From 3c9b28b4a0aa01ea6d93f3bfbda0cd0fbb37de99 Mon Sep 17 00:00:00 2001 From: David Melbye Wechsler Date: Wed, 17 Aug 2022 10:27:56 +0200 Subject: [PATCH 01/10] feat(1090/default_tz): Added possibility to override default UTC - #1090. --- arrow/api.py | 7 +++ arrow/arrow.py | 124 +++++++++++++++++++++++++++--------------- arrow/factory.py | 55 ++++++++----------- arrow/util.py | 40 +++++++++++++- docs/index.rst | 10 ++++ tests/test_arrow.py | 44 +++++++++++++++ tests/test_factory.py | 39 +++++++++++++ 7 files changed, 243 insertions(+), 76 deletions(-) diff --git a/arrow/api.py b/arrow/api.py index d8ed24b9..096c6f6e 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -9,6 +9,8 @@ from time import struct_time from typing import Any, List, Optional, Tuple, Type, Union, overload +from dateutil import tz as dateutil_tz + from arrow.arrow import TZ_EXPR, Arrow from arrow.constants import DEFAULT_LOCALE from arrow.factory import ArrowFactory @@ -26,6 +28,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, + default_tz: Optional[TZ_EXPR] = None, ) -> Arrow: ... # pragma: no cover @@ -36,6 +39,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, + default_tz: Optional[TZ_EXPR] = None, ) -> Arrow: ... # pragma: no cover @@ -57,6 +61,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, + default_tz: Optional[TZ_EXPR] = None, ) -> Arrow: ... # pragma: no cover @@ -69,6 +74,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, + default_tz: Optional[TZ_EXPR] = None, ) -> Arrow: ... # pragma: no cover @@ -81,6 +87,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, + default_tz: Optional[TZ_EXPR] = None, ) -> Arrow: ... # pragma: no cover diff --git a/arrow/arrow.py b/arrow/arrow.py index 1ede107f..a83391c0 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -93,7 +93,8 @@ class Arrow: :param minute: (optional) the minute, Defaults to 0. :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 default_tz: A timezone expression that will be used on naive datetimes. Defaults to UTC. + :param tzinfo: (optional) A timezone expression. :param fold: (optional) 0 or 1, used to disambiguate repeated wall times. Defaults to 0. .. _tz-expr: @@ -148,6 +149,8 @@ class Arrow: } _datetime: dt_datetime + default_tz: TZ_EXPR + default_tz_used: bool def __init__( self, @@ -159,20 +162,15 @@ def __init__( second: int = 0, microsecond: int = 0, tzinfo: Optional[TZ_EXPR] = None, + default_tz: TZ_EXPR = dateutil_tz.tzutc(), + default_tz_used: bool = False, **kwargs: Any, ) -> None: - 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 hasattr(tzinfo, "zone") - and tzinfo.zone # type: ignore[attr-defined] - ): - tzinfo = parser.TzinfoParser.parse(tzinfo.zone) # type: ignore[attr-defined] - elif isinstance(tzinfo, str): - tzinfo = parser.TzinfoParser.parse(tzinfo) + self.default_tz = default_tz + self.default_tz_used = default_tz_used + # If default_tz_used is already set, tzinfo should also already be set + if not default_tz_used: + tzinfo, self.default_tz_used = util.get_tzinfo_default_used(default_tz=self.default_tz, tzinfo=tzinfo) fold = kwargs.get("fold", 0) @@ -183,11 +181,17 @@ def __init__( # factories: single object, both original and from datetime. @classmethod - def now(cls, tzinfo: Optional[dt_tzinfo] = None) -> "Arrow": + def now( + cls, + tzinfo: Optional[dt_tzinfo] = None, + default_tz: Optional[TZ_EXPR] = None, + ) -> "Arrow": """Constructs an :class:`Arrow ` object, representing "now" in the given timezone. - :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. + :param tzinfo: (optional) a ``tzinfo`` object. + :param default_tz: (optional) a :ref:`timezone expression ` that will be used on naive datetimes. + Defaults to local timezone. Usage:: @@ -196,8 +200,10 @@ def now(cls, tzinfo: Optional[dt_tzinfo] = None) -> "Arrow": """ - if tzinfo is None: - tzinfo = dateutil_tz.tzlocal() + if default_tz is None: + default_tz = dateutil_tz.tzlocal() + + tzinfo, default_tz_used = util.get_tzinfo_default_used(default_tz=default_tz, tzinfo=tzinfo) dt = dt_datetime.now(tzinfo) @@ -209,7 +215,9 @@ def now(cls, tzinfo: Optional[dt_tzinfo] = None) -> "Arrow": dt.minute, dt.second, dt.microsecond, - dt.tzinfo, + tzinfo=dt.tzinfo, + default_tz=default_tz, + default_tz_used=default_tz_used, fold=getattr(dt, "fold", 0), ) @@ -235,7 +243,7 @@ def utcnow(cls) -> "Arrow": dt.minute, dt.second, dt.microsecond, - dt.tzinfo, + tzinfo=dt.tzinfo, fold=getattr(dt, "fold", 0), ) @@ -244,23 +252,25 @@ def fromtimestamp( cls, timestamp: Union[int, float, str], tzinfo: Optional[TZ_EXPR] = None, + default_tz: Optional[TZ_EXPR] = None, ) -> "Arrow": """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. - + :param tzinfo: (optional) a ``tzinfo`` object. + :param default_tz: (optional) a :ref:`timezone expression ` that will be used on naive datetimes. + Defaults to local timezone. """ - if tzinfo is None: - tzinfo = dateutil_tz.tzlocal() - elif isinstance(tzinfo, str): - tzinfo = parser.TzinfoParser.parse(tzinfo) - if not util.is_timestamp(timestamp): raise ValueError(f"The provided timestamp {timestamp!r} is invalid.") + if default_tz is None: + default_tz = dateutil_tz.tzlocal() + + tzinfo, default_tz_used = util.get_tzinfo_default_used(default_tz=default_tz, tzinfo=tzinfo) + timestamp = util.normalize_timestamp(float(timestamp)) dt = dt_datetime.fromtimestamp(timestamp, tzinfo) @@ -272,7 +282,9 @@ def fromtimestamp( dt.minute, dt.second, dt.microsecond, - dt.tzinfo, + tzinfo=dt.tzinfo, + default_tz=default_tz, + default_tz_used=default_tz_used, fold=getattr(dt, "fold", 0), ) @@ -298,18 +310,25 @@ def utcfromtimestamp(cls, timestamp: Union[int, float, str]) -> "Arrow": dt.minute, dt.second, dt.microsecond, - dateutil_tz.tzutc(), + tzinfo=dateutil_tz.tzutc(), fold=getattr(dt, "fold", 0), ) @classmethod - def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": + def fromdatetime( + cls, + dt: dt_datetime, + tzinfo: Optional[TZ_EXPR] = None, + default_tz: Optional[TZ_EXPR] = None, + ) -> "Arrow": """Constructs an :class:`Arrow ` object from a ``datetime`` and optional replacement timezone. :param dt: the ``datetime`` :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to ``dt``'s - timezone, or UTC if naive. + timezone, or ``default_tz`` if naive. + :param default_tz: (optional) a :ref:`timezone expression ` that will be used on naive datetimes. + Defaults to UTC. Usage:: @@ -320,11 +339,10 @@ def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arr """ - if tzinfo is None: - if dt.tzinfo is None: - tzinfo = dateutil_tz.tzutc() - else: - tzinfo = dt.tzinfo + if default_tz is None: + default_tz = dateutil_tz.tzutc() + + tzinfo, default_tz_used = util.get_tzinfo_default_used(default_tz=default_tz, dt=dt, tzinfo=tzinfo) return cls( dt.year, @@ -334,24 +352,42 @@ def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arr dt.minute, dt.second, dt.microsecond, - tzinfo, + tzinfo=tzinfo, + default_tz=default_tz, + default_tz_used=default_tz_used, fold=getattr(dt, "fold", 0), ) @classmethod - def fromdate(cls, date: date, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": + def fromdate( + cls, + date: date, + tzinfo: Optional[TZ_EXPR] = None, + default_tz: Optional[TZ_EXPR] = None, + ) -> "Arrow": """Constructs an :class:`Arrow ` object from a ``date`` and optional replacement timezone. All time values are set to 0. :param date: the ``date`` - :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to UTC. + :param tzinfo: (optional) A :ref:`timezone expression `. + :param default_tz: (optional) a :ref:`timezone expression ` that will be used on naive datetimes. + Defaults to UTC. """ - if tzinfo is None: - tzinfo = dateutil_tz.tzutc() + if default_tz is None: + default_tz = dateutil_tz.tzutc() + + tzinfo, default_tz_used = util.get_tzinfo_default_used(default_tz=default_tz, tzinfo=tzinfo) - return cls(date.year, date.month, date.day, tzinfo=tzinfo) + return cls( + date.year, + date.month, + date.day, + tzinfo=tzinfo, + default_tz=default_tz, + default_tz_used=default_tz_used, + ) @classmethod def strptime( @@ -384,7 +420,7 @@ def strptime( dt.minute, dt.second, dt.microsecond, - tzinfo, + tzinfo=tzinfo, fold=getattr(dt, "fold", 0), ) @@ -412,7 +448,7 @@ def fromordinal(cls, ordinal: int) -> "Arrow": dt.minute, dt.second, dt.microsecond, - dt.tzinfo, + tzinfo=dt.tzinfo, fold=getattr(dt, "fold", 0), ) @@ -1086,7 +1122,7 @@ def to(self, tz: TZ_EXPR) -> "Arrow": dt.minute, dt.second, dt.microsecond, - dt.tzinfo, + tzinfo=dt.tzinfo, fold=getattr(dt, "fold", 0), ) diff --git a/arrow/factory.py b/arrow/factory.py index aad4af8b..adfe559d 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -41,6 +41,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, + default_tz: Optional[TZ_EXPR] = None, ) -> Arrow: ... # pragma: no cover @@ -62,6 +63,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, + default_tz: Optional[TZ_EXPR] = None, ) -> Arrow: ... # pragma: no cover @@ -74,6 +76,7 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, + default_tz: Optional[TZ_EXPR] = None, ) -> Arrow: ... # pragma: no cover @@ -86,19 +89,21 @@ def get( locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, normalize_whitespace: bool = False, + default_tz: Optional[TZ_EXPR] = None, ) -> 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'. + :param locale: (optional) a ``s :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. + the timezone in a positional argument. :param normalize_whitespace: (optional) a ``bool`` specifying whether or not to normalize redundant whitespace (spaces, tabs, and newlines) in a datetime string before parsing. - Defaults to false. + Defaults to false.tr`` specifying a locale for the parser. Defaults to 'en-us'. + :param default_tz: (optional) a :ref:`timezone expression ` that will be used on naive datetimes. Usage:: @@ -196,6 +201,7 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: arg_count = len(args) locale = kwargs.pop("locale", DEFAULT_LOCALE) tz = kwargs.get("tzinfo", None) + default_tz = kwargs.get("default_tz", None) normalize_whitespace = kwargs.pop("normalize_whitespace", False) # if kwargs given, send to constructor unless only tzinfo provided @@ -203,19 +209,14 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: arg_count = 3 # tzinfo kwarg is not provided - if len(kwargs) == 1 and tz is None: + if len(kwargs) == 1 and tz is None and default_tz is None: arg_count = 3 - # () -> now, @ tzinfo or utc + # () -> now, @ tzinfo or default_tz or utc if arg_count == 0: - if isinstance(tz, str): - tz = parser.TzinfoParser.parse(tz) - return self.type.now(tzinfo=tz) - - if isinstance(tz, dt_tzinfo): - return self.type.now(tzinfo=tz) - - return self.type.utcnow() + if default_tz is None: + default_tz = dateutil_tz.tzutc() + return self.type.now(default_tz=default_tz, tzinfo=tz) if arg_count == 1: arg = args[0] @@ -228,22 +229,19 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: # 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) + return self.type.fromtimestamp(arg, default_tz=default_tz, tzinfo=tz) # (Arrow) -> from the object's datetime @ tzinfo elif isinstance(arg, Arrow): - return self.type.fromdatetime(arg.datetime, tzinfo=tz) + return self.type.fromdatetime(arg.datetime, default_tz=default_tz, tzinfo=tz) # (datetime) -> from datetime @ tzinfo elif isinstance(arg, datetime): - return self.type.fromdatetime(arg, tzinfo=tz) + return self.type.fromdatetime(arg, default_tz=default_tz, tzinfo=tz) # (date) -> from date @ tzinfo elif isinstance(arg, date): - return self.type.fromdate(arg, tzinfo=tz) + return self.type.fromdate(arg, default_tz=default_tz, tzinfo=tz) # (tzinfo) -> now @ tzinfo elif isinstance(arg, dt_tzinfo): @@ -252,7 +250,7 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: # (str) -> parse @ tzinfo elif isinstance(arg, str): dt = parser.DateTimeParser(locale).parse_iso(arg, normalize_whitespace) - return self.type.fromdatetime(dt, tzinfo=tz) + return self.type.fromdatetime(dt, default_tz=default_tz, tzinfo=tz) # (struct_time) -> from struct_time elif isinstance(arg, struct_time): @@ -261,7 +259,7 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: # (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, tzinfo=tz) + return self.type.fromdate(d, default_tz=default_tz, tzinfo=tz) else: raise TypeError(f"Cannot parse single argument of type {type(arg)!r}.") @@ -274,7 +272,7 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: # (datetime, tzinfo/str) -> fromdatetime @ tzinfo if isinstance(arg_2, (dt_tzinfo, str)): - return self.type.fromdatetime(arg_1, tzinfo=arg_2) + return self.type.fromdatetime(arg_1, default_tz=default_tz, tzinfo=arg_2) else: raise TypeError( f"Cannot parse two arguments of types 'datetime', {type(arg_2)!r}." @@ -284,7 +282,7 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: # (date, tzinfo/str) -> fromdate @ tzinfo if isinstance(arg_2, (dt_tzinfo, str)): - return self.type.fromdate(arg_1, tzinfo=arg_2) + return self.type.fromdate(arg_1, default_tz=default_tz, tzinfo=arg_2) else: raise TypeError( f"Cannot parse two arguments of types 'date', {type(arg_2)!r}." @@ -295,7 +293,7 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: dt = parser.DateTimeParser(locale).parse( args[0], args[1], normalize_whitespace ) - return self.type.fromdatetime(dt, tzinfo=tz) + return self.type.fromdatetime(dt, default_tz=default_tz, tzinfo=tz) else: raise TypeError( @@ -340,9 +338,4 @@ def now(self, tz: Optional[TZ_EXPR] = None) -> Arrow: """ - if tz is None: - tz = dateutil_tz.tzlocal() - elif not isinstance(tz, dt_tzinfo): - tz = parser.TzinfoParser.parse(tz) - - return self.type.now(tz) + return self.type.now(default_tz=dateutil_tz.tzlocal(), tzinfo=tz) diff --git a/arrow/util.py b/arrow/util.py index f3eaa21c..20d994ee 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -1,10 +1,12 @@ """Helpful functions used internally within arrow.""" import datetime -from typing import Any, Optional, cast +from datetime import tzinfo as dt_tzinfo, datetime as dt_datetime +from typing import Any, Optional, cast, Tuple, Union from dateutil.rrule import WEEKLY, rrule +from arrow import parser from arrow.constants import ( MAX_ORDINAL, MAX_TIMESTAMP, @@ -115,3 +117,39 @@ def validate_bounds(bounds: str) -> None: __all__ = ["next_weekday", "is_timestamp", "validate_ordinal", "iso_to_gregorian"] + + +def get_tzinfo_default_used( + default_tz: Union[dt_tzinfo, str], + dt: Optional[dt_datetime] = None, + tzinfo: Optional[Union[dt_tzinfo, str]] = None +) -> Tuple[dt_tzinfo, bool]: + """Get the tzinfo to use, and indicate if it was based on ``default_tz``. + + :param default_tz: A :ref:`timezone expression ` that will be used on naive datetimes. + :param dt: (optional) a ``datetime`` object. + :param tzinfo: (optional) a ``tzinfo`` object. + """ + + default_tz_used = False + + if tzinfo is None: + if dt and dt.tzinfo: + tzinfo = dt.tzinfo + + else: + tzinfo = default_tz + default_tz_used = True + + # detect that tzinfo is a pytz object (issue #626) + if ( + isinstance(tzinfo, dt_tzinfo) + and hasattr(tzinfo, "localize") + and hasattr(tzinfo, "zone") + and tzinfo.zone # type: ignore[attr-defined] + ): + tzinfo = parser.TzinfoParser.parse(tzinfo.zone) # type: ignore[attr-defined] + elif isinstance(tzinfo, str): + tzinfo = parser.TzinfoParser.parse(tzinfo) + + return tzinfo, default_tz_used diff --git a/docs/index.rst b/docs/index.rst index d4f9ec2a..f8be2cdf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -60,6 +60,16 @@ Parse from a string: >>> arrow.get('2013-05-05 12:30:45', 'YYYY-MM-DD HH:mm:ss') +It is possible to override the default UTC timezone for a naive datetime +(any timezone-aware datetime will keep it's original timezone): + +.. code-block:: python + + >>> arrow.get('2013-05-05 12:30:45', 'YYYY-MM-DD HH:mm:ss', default_tz='US/Pacific') + + >>> arrow.get('2013-05-05 12:30:45Z', 'YYYY-MM-DD HH:mm:ssZ', default_tz='US/Pacific') + + Search a date in a string: .. code-block:: python diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 38d5000a..5e38dff3 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -81,6 +81,50 @@ def test_init_with_fold(self): assert before == after assert before.utcoffset() != after.utcoffset() + def test_init_default_tz(self): + + result = arrow.Arrow(2013, 2, 2, 12, 30, 45, 999999, default_tz=tz.gettz("Europe/Oslo")) + self.expected = datetime( + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Oslo") + ) + assert result._datetime == self.expected + assert_datetime_equality(result._datetime, self.expected, 1) + assert result.default_tz_used + assert result.default_tz == tz.gettz("Europe/Oslo") + + def test_init_default_tz_string(self): + + result = arrow.Arrow(2013, 2, 2, 12, 30, 45, 999999, default_tz="Europe/Oslo") + self.expected = datetime( + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Oslo") + ) + assert result._datetime == self.expected + assert_datetime_equality(result._datetime, self.expected, 1) + assert result.default_tz_used + assert result.default_tz == "Europe/Oslo" + + def test_init_default_tz_none(self): + + result = arrow.Arrow(2013, 2, 2, 12, 30, 45, 999999) + self.expected = datetime( + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.tzutc() + ) + assert result._datetime == self.expected + assert_datetime_equality(result._datetime, self.expected, 1) + assert result.default_tz_used is True + assert result.default_tz == tz.tzutc() + + def test_init_default_tz_existing_tzinfo(self): + + result = arrow.Arrow(2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.tzutc(), default_tz=tz.gettz("Europe/Oslo")) + self.expected = datetime( + 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.tzutc() + ) + assert result._datetime == self.expected + assert_datetime_equality(result._datetime, self.expected, 1) + assert result.default_tz_used is False + assert result.default_tz == tz.gettz("Europe/Oslo") + class TestTestArrowFactory: def test_now(self): diff --git a/tests/test_factory.py b/tests/test_factory.py index f368126c..9547a0bb 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -90,6 +90,15 @@ def test_one_arg_timestamp_with_tzinfo(self): self.factory.get(timestamp, tzinfo=timezone), timestamp_dt ) + def test_one_arg_str(self): + + result = self.factory.get("1990-01-01 12:30:45") + self.expected = datetime(1990, 1, 1, 12, 30, 45).replace(tzinfo=tz.tzutc()) + + assert_datetime_equality(result, self.expected) + assert result.default_tz == tz.tzutc() + assert result.default_tz_used + def test_one_arg_arrow(self): arw = self.factory.utcnow() @@ -156,6 +165,36 @@ def test_kwarg_tzinfo_string(self): with pytest.raises(ParserError): self.factory.get(tzinfo="US/PacificInvalidTzinfo") + def test_kwarg_default_tz(self): + + self.expected = ( + datetime.now() + .astimezone(tz.gettz("Europe/Oslo")) + ) + + assert_datetime_equality( + self.factory.get(default_tz=tz.gettz("Europe/Oslo")), self.expected + ) + + def test_kwarg_default_tz_string(self): + + self.expected = ( + datetime.now() + .astimezone(tz.gettz("Europe/Oslo")) + ) + + assert_datetime_equality(self.factory.get(default_tz="Europe/Oslo"), self.expected) + + with pytest.raises(ParserError): + self.factory.get(default_tz="Europe/OsloInvalidTzinfo") + + def test_kwarg_default_tz_string_existing_tzinfo(self): + + self.expected = datetime(1990, 1, 1, 12, 30, 45).replace(tzinfo=tz.tzutc()) + + assert_datetime_equality(self.factory.get("1990-01-01 12:30:45+00:00", default_tz="Europe/Oslo"), self.expected) + + def test_kwarg_normalize_whitespace(self): result = self.factory.get( "Jun 1 2005 1:33PM", From a7c609779c40c863cbdcf30fb3b4fadff42fc28f Mon Sep 17 00:00:00 2001 From: David Melbye Wechsler Date: Wed, 24 Aug 2022 22:45:29 +0200 Subject: [PATCH 02/10] fix(1090/default_tz): Fixed default to UTC for single timestamp arg to ArrowFactory.get - #1090. --- arrow/factory.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/arrow/factory.py b/arrow/factory.py index adfe559d..c6677d79 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -220,15 +220,19 @@ 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: raise TypeError("Cannot parse argument of type None.") + if isinstance(arg, Decimal): + arg = float(arg) + # try (int, float) -> from timestamp @ tzinfo - elif not isinstance(arg, str) and is_timestamp(arg): + if not isinstance(arg, str) and is_timestamp(arg): + if default_tz is None: + # set to UTC by default + default_tz = dateutil_tz.tzutc() return self.type.fromtimestamp(arg, default_tz=default_tz, tzinfo=tz) # (Arrow) -> from the object's datetime @ tzinfo From 25b558745f2613eed8e1f5de9a8e6032c18f5995 Mon Sep 17 00:00:00 2001 From: David Melbye Wechsler Date: Tue, 30 Aug 2022 23:33:57 +0200 Subject: [PATCH 03/10] fix(1090/default_tz): Fixed lint errors and coverage - #1090. --- arrow/api.py | 9 +++---- arrow/arrow.py | 63 +++++++++++++++++++++++++++---------------- arrow/constants.py | 5 +++- arrow/factory.py | 12 ++++++--- tests/test_arrow.py | 38 +++++++++++++++++++++----- tests/test_factory.py | 45 +++++++++++++++++++++++-------- 6 files changed, 121 insertions(+), 51 deletions(-) diff --git a/arrow/api.py b/arrow/api.py index 096c6f6e..fdb072cc 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -4,14 +4,11 @@ """ -from datetime import date, datetime -from datetime import tzinfo as dt_tzinfo +from datetime import date, datetime, tzinfo as dt_tzinfo from time import struct_time -from typing import Any, List, Optional, Tuple, Type, Union, overload +from typing import Any, List, Optional, overload, Tuple, Type, Union -from dateutil import tz as dateutil_tz - -from arrow.arrow import TZ_EXPR, Arrow +from arrow.arrow import Arrow, TZ_EXPR from arrow.constants import DEFAULT_LOCALE from arrow.factory import ArrowFactory diff --git a/arrow/arrow.py b/arrow/arrow.py index a83391c0..3a5bb0f9 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -8,32 +8,34 @@ import calendar import re import sys -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 datetime import ( + date, + datetime as dt_datetime, + time as dt_time, + timedelta, + tzinfo as dt_tzinfo, +) from math import trunc from time import struct_time from typing import ( Any, + cast, ClassVar, Generator, Iterable, List, Mapping, Optional, + overload, 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.constants import DEFAULT_LOCALE, DEHUMANIZE_LOCALES +from arrow.constants import DEFAULT_LOCALE, DEFAULT_TZ, DEHUMANIZE_LOCALES from arrow.locales import TimeFrameLiteral if sys.version_info < (3, 8): # pragma: no cover @@ -162,7 +164,7 @@ def __init__( second: int = 0, microsecond: int = 0, tzinfo: Optional[TZ_EXPR] = None, - default_tz: TZ_EXPR = dateutil_tz.tzutc(), + default_tz: TZ_EXPR = DEFAULT_TZ, default_tz_used: bool = False, **kwargs: Any, ) -> None: @@ -170,7 +172,14 @@ def __init__( self.default_tz_used = default_tz_used # If default_tz_used is already set, tzinfo should also already be set if not default_tz_used: - tzinfo, self.default_tz_used = util.get_tzinfo_default_used(default_tz=self.default_tz, tzinfo=tzinfo) + tzinfo, self.default_tz_used = util.get_tzinfo_default_used( + default_tz=self.default_tz, tzinfo=tzinfo + ) + + # Cast due to mypy error + # Argument 8 to "datetime" has incompatible type + # "Union[tzinfo, str, None]"; expected "Optional[tzinfo]" + cast(dt_tzinfo, tzinfo) fold = kwargs.get("fold", 0) @@ -183,7 +192,7 @@ def __init__( @classmethod def now( cls, - tzinfo: Optional[dt_tzinfo] = None, + tzinfo: Optional[TZ_EXPR] = None, default_tz: Optional[TZ_EXPR] = None, ) -> "Arrow": """Constructs an :class:`Arrow ` object, representing "now" in the given @@ -203,7 +212,9 @@ def now( if default_tz is None: default_tz = dateutil_tz.tzlocal() - tzinfo, default_tz_used = util.get_tzinfo_default_used(default_tz=default_tz, tzinfo=tzinfo) + tzinfo, default_tz_used = util.get_tzinfo_default_used( + default_tz=default_tz, tzinfo=tzinfo + ) dt = dt_datetime.now(tzinfo) @@ -269,7 +280,9 @@ def fromtimestamp( if default_tz is None: default_tz = dateutil_tz.tzlocal() - tzinfo, default_tz_used = util.get_tzinfo_default_used(default_tz=default_tz, tzinfo=tzinfo) + tzinfo, default_tz_used = util.get_tzinfo_default_used( + default_tz=default_tz, tzinfo=tzinfo + ) timestamp = util.normalize_timestamp(float(timestamp)) dt = dt_datetime.fromtimestamp(timestamp, tzinfo) @@ -316,10 +329,10 @@ def utcfromtimestamp(cls, timestamp: Union[int, float, str]) -> "Arrow": @classmethod def fromdatetime( - cls, - dt: dt_datetime, - tzinfo: Optional[TZ_EXPR] = None, - default_tz: Optional[TZ_EXPR] = None, + cls, + dt: dt_datetime, + tzinfo: Optional[TZ_EXPR] = None, + default_tz: Optional[TZ_EXPR] = None, ) -> "Arrow": """Constructs an :class:`Arrow ` object from a ``datetime`` and optional replacement timezone. @@ -342,7 +355,9 @@ def fromdatetime( if default_tz is None: default_tz = dateutil_tz.tzutc() - tzinfo, default_tz_used = util.get_tzinfo_default_used(default_tz=default_tz, dt=dt, tzinfo=tzinfo) + tzinfo, default_tz_used = util.get_tzinfo_default_used( + default_tz=default_tz, dt=dt, tzinfo=tzinfo + ) return cls( dt.year, @@ -360,10 +375,10 @@ def fromdatetime( @classmethod def fromdate( - cls, - date: date, - tzinfo: Optional[TZ_EXPR] = None, - default_tz: Optional[TZ_EXPR] = None, + cls, + date: date, + tzinfo: Optional[TZ_EXPR] = None, + default_tz: Optional[TZ_EXPR] = None, ) -> "Arrow": """Constructs an :class:`Arrow ` object from a ``date`` and optional replacement timezone. All time values are set to 0. @@ -378,7 +393,9 @@ def fromdate( if default_tz is None: default_tz = dateutil_tz.tzutc() - tzinfo, default_tz_used = util.get_tzinfo_default_used(default_tz=default_tz, tzinfo=tzinfo) + tzinfo, default_tz_used = util.get_tzinfo_default_used( + default_tz=default_tz, tzinfo=tzinfo + ) return cls( date.year, diff --git a/arrow/constants.py b/arrow/constants.py index 53d163b9..fb1ecedd 100644 --- a/arrow/constants.py +++ b/arrow/constants.py @@ -1,7 +1,9 @@ """Constants used internally in arrow.""" import sys -from datetime import datetime +from datetime import datetime, tzinfo + +from dateutil import tz if sys.version_info < (3, 8): # pragma: no cover from typing_extensions import Final @@ -35,6 +37,7 @@ MAX_ORDINAL: Final[int] = datetime.max.toordinal() MIN_ORDINAL: Final[int] = 1 +DEFAULT_TZ: tzinfo = tz.tzutc() DEFAULT_LOCALE: Final[str] = "en-us" # Supported dehumanize locales diff --git a/arrow/factory.py b/arrow/factory.py index c6677d79..7c87f205 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -237,7 +237,9 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: # (Arrow) -> from the object's datetime @ tzinfo elif isinstance(arg, Arrow): - return self.type.fromdatetime(arg.datetime, default_tz=default_tz, tzinfo=tz) + return self.type.fromdatetime( + arg.datetime, default_tz=default_tz, tzinfo=tz + ) # (datetime) -> from datetime @ tzinfo elif isinstance(arg, datetime): @@ -276,7 +278,9 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: # (datetime, tzinfo/str) -> fromdatetime @ tzinfo if isinstance(arg_2, (dt_tzinfo, str)): - return self.type.fromdatetime(arg_1, default_tz=default_tz, tzinfo=arg_2) + return self.type.fromdatetime( + arg_1, default_tz=default_tz, tzinfo=arg_2 + ) else: raise TypeError( f"Cannot parse two arguments of types 'datetime', {type(arg_2)!r}." @@ -286,7 +290,9 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: # (date, tzinfo/str) -> fromdate @ tzinfo if isinstance(arg_2, (dt_tzinfo, str)): - return self.type.fromdate(arg_1, default_tz=default_tz, tzinfo=arg_2) + return self.type.fromdate( + arg_1, default_tz=default_tz, tzinfo=arg_2 + ) else: raise TypeError( f"Cannot parse two arguments of types 'date', {type(arg_2)!r}." diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 5e38dff3..cfd1cf1a 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -83,7 +83,9 @@ def test_init_with_fold(self): def test_init_default_tz(self): - result = arrow.Arrow(2013, 2, 2, 12, 30, 45, 999999, default_tz=tz.gettz("Europe/Oslo")) + result = arrow.Arrow( + 2013, 2, 2, 12, 30, 45, 999999, default_tz=tz.gettz("Europe/Oslo") + ) self.expected = datetime( 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.gettz("Europe/Oslo") ) @@ -106,9 +108,7 @@ def test_init_default_tz_string(self): def test_init_default_tz_none(self): result = arrow.Arrow(2013, 2, 2, 12, 30, 45, 999999) - self.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()) assert result._datetime == self.expected assert_datetime_equality(result._datetime, self.expected, 1) assert result.default_tz_used is True @@ -116,10 +116,18 @@ def test_init_default_tz_none(self): def test_init_default_tz_existing_tzinfo(self): - result = arrow.Arrow(2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.tzutc(), default_tz=tz.gettz("Europe/Oslo")) - self.expected = datetime( - 2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.tzutc() + result = arrow.Arrow( + 2013, + 2, + 2, + 12, + 30, + 45, + 999999, + tzinfo=tz.tzutc(), + default_tz=tz.gettz("Europe/Oslo"), ) + self.expected = datetime(2013, 2, 2, 12, 30, 45, 999999, tzinfo=tz.tzutc()) assert result._datetime == self.expected assert_datetime_equality(result._datetime, self.expected, 1) assert result.default_tz_used is False @@ -213,6 +221,9 @@ def test_fromdate(self): assert result._datetime == datetime(2013, 2, 3, tzinfo=tz.gettz("US/Pacific")) + result = arrow.Arrow.fromdate(dt, default_tz=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") @@ -332,6 +343,19 @@ def test_tzinfo(self): assert self.arrow.tzinfo == tz.tzutc() + def test_default_tz(self): + + assert self.arrow.default_tz == tz.tzutc() + assert self.arrow.default_tz_used is True + + result = arrow.Arrow(2013, 1, 1, tzinfo=tz.gettz("Europe/Oslo")) + assert result.default_tz == tz.tzutc() + assert result.default_tz_used is False + + result = arrow.Arrow(2013, 1, 1, default_tz=tz.gettz("Europe/Oslo")) + assert result.default_tz == tz.gettz("Europe/Oslo") + assert result.default_tz_used + def test_naive(self): assert self.arrow.naive == self.arrow._datetime.replace(tzinfo=None) diff --git a/tests/test_factory.py b/tests/test_factory.py index 9547a0bb..c66e9478 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -90,6 +90,18 @@ def test_one_arg_timestamp_with_tzinfo(self): self.factory.get(timestamp, tzinfo=timezone), timestamp_dt ) + def test_one_arg_timestamp_with_default_tz(self): + + timestamp = time.time() + timestamp_dt = datetime.fromtimestamp(timestamp, tz=tz.tzutc()).astimezone( + tz.gettz("US/Pacific") + ) + timezone = tz.gettz("US/Pacific") + + assert_datetime_equality( + self.factory.get(timestamp, default_tz=timezone), timestamp_dt + ) + def test_one_arg_str(self): result = self.factory.get("1990-01-01 12:30:45") @@ -167,10 +179,7 @@ def test_kwarg_tzinfo_string(self): def test_kwarg_default_tz(self): - self.expected = ( - datetime.now() - .astimezone(tz.gettz("Europe/Oslo")) - ) + self.expected = datetime.now().astimezone(tz.gettz("Europe/Oslo")) assert_datetime_equality( self.factory.get(default_tz=tz.gettz("Europe/Oslo")), self.expected @@ -178,12 +187,11 @@ def test_kwarg_default_tz(self): def test_kwarg_default_tz_string(self): - self.expected = ( - datetime.now() - .astimezone(tz.gettz("Europe/Oslo")) - ) + self.expected = datetime.now().astimezone(tz.gettz("Europe/Oslo")) - assert_datetime_equality(self.factory.get(default_tz="Europe/Oslo"), self.expected) + assert_datetime_equality( + self.factory.get(default_tz="Europe/Oslo"), self.expected + ) with pytest.raises(ParserError): self.factory.get(default_tz="Europe/OsloInvalidTzinfo") @@ -192,8 +200,10 @@ def test_kwarg_default_tz_string_existing_tzinfo(self): self.expected = datetime(1990, 1, 1, 12, 30, 45).replace(tzinfo=tz.tzutc()) - assert_datetime_equality(self.factory.get("1990-01-01 12:30:45+00:00", default_tz="Europe/Oslo"), self.expected) - + assert_datetime_equality( + self.factory.get("1990-01-01 12:30:45+00:00", default_tz="Europe/Oslo"), + self.expected, + ) def test_kwarg_normalize_whitespace(self): result = self.factory.get( @@ -245,6 +255,19 @@ def test_one_arg_date_tzinfo_kwarg(self): assert result.date() == expected.date() assert result.tzinfo == expected.tzinfo + def test_one_arg_date_default_tz_kwarg(self): + + da = date(2021, 4, 29) + + result = self.factory.get(da, default_tz="America/Chicago") + + expected = Arrow(2021, 4, 29, tzinfo=tz.gettz("America/Chicago")) + + assert result.date() == expected.date() + assert result.tzinfo == expected.tzinfo + assert result.default_tz == "America/Chicago" + assert result.default_tz_used is True + def test_one_arg_iso_calendar_tzinfo_kwarg(self): result = self.factory.get((2004, 1, 7), tzinfo="America/Chicago") From 4ec570cc4ae36de300ed457fe3f342cf27305627 Mon Sep 17 00:00:00 2001 From: David Melbye Wechsler Date: Sat, 3 Sep 2022 21:22:53 +0200 Subject: [PATCH 04/10] fix(1090/default_tz): Fixed lint error - #1090. --- arrow/arrow.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 3a5bb0f9..8009d5cc 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -170,17 +170,17 @@ def __init__( ) -> None: self.default_tz = default_tz self.default_tz_used = default_tz_used - # If default_tz_used is already set, tzinfo should also already be set - if not default_tz_used: + # If tzinfo is not a datetime.tzinfo object parse tzinfo + # Also detects if tzinfo is a pytz object (issue #626) + if not isinstance(tzinfo, dt_tzinfo) or ( + hasattr(tzinfo, "localize") + and hasattr(tzinfo, "zone") + and tzinfo.zone # type: ignore[attr-defined] + ): tzinfo, self.default_tz_used = util.get_tzinfo_default_used( default_tz=self.default_tz, tzinfo=tzinfo ) - # Cast due to mypy error - # Argument 8 to "datetime" has incompatible type - # "Union[tzinfo, str, None]"; expected "Optional[tzinfo]" - cast(dt_tzinfo, tzinfo) - fold = kwargs.get("fold", 0) self._datetime = dt_datetime( From a51f72d176bdf3c726ca8bf3ce5c3dbeb999ba81 Mon Sep 17 00:00:00 2001 From: David Melbye Wechsler Date: Sun, 4 Sep 2022 14:29:33 +0200 Subject: [PATCH 05/10] fix(1090/default_tz): Fixed isort and black errors - #1090. --- arrow/api.py | 7 ++++--- arrow/arrow.py | 16 +++++++--------- arrow/util.py | 19 ++++++++++--------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/arrow/api.py b/arrow/api.py index fdb072cc..8d6e0509 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -4,11 +4,12 @@ """ -from datetime import date, datetime, tzinfo as dt_tzinfo +from datetime import date, datetime +from datetime import tzinfo as dt_tzinfo from time import struct_time -from typing import Any, List, Optional, overload, Tuple, Type, Union +from typing import Any, List, Optional, Tuple, Type, Union, overload -from arrow.arrow import Arrow, TZ_EXPR +from arrow.arrow import TZ_EXPR, Arrow from arrow.constants import DEFAULT_LOCALE from arrow.factory import ArrowFactory diff --git a/arrow/arrow.py b/arrow/arrow.py index 8009d5cc..6db278c9 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -8,27 +8,25 @@ import calendar import re import sys -from datetime import ( - date, - datetime as dt_datetime, - time as dt_time, - timedelta, - tzinfo as dt_tzinfo, -) +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, - cast, ClassVar, Generator, Iterable, List, Mapping, Optional, - overload, Tuple, Union, + cast, + overload, ) from dateutil import tz as dateutil_tz diff --git a/arrow/util.py b/arrow/util.py index 20d994ee..084bc780 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -1,8 +1,9 @@ """Helpful functions used internally within arrow.""" import datetime -from datetime import tzinfo as dt_tzinfo, datetime as dt_datetime -from typing import Any, Optional, cast, Tuple, Union +from datetime import datetime as dt_datetime +from datetime import tzinfo as dt_tzinfo +from typing import Any, Optional, Tuple, Union, cast from dateutil.rrule import WEEKLY, rrule @@ -120,9 +121,9 @@ def validate_bounds(bounds: str) -> None: def get_tzinfo_default_used( - default_tz: Union[dt_tzinfo, str], - dt: Optional[dt_datetime] = None, - tzinfo: Optional[Union[dt_tzinfo, str]] = None + default_tz: Union[dt_tzinfo, str], + dt: Optional[dt_datetime] = None, + tzinfo: Optional[Union[dt_tzinfo, str]] = None, ) -> Tuple[dt_tzinfo, bool]: """Get the tzinfo to use, and indicate if it was based on ``default_tz``. @@ -143,10 +144,10 @@ def get_tzinfo_default_used( # detect that tzinfo is a pytz object (issue #626) if ( - isinstance(tzinfo, dt_tzinfo) - and hasattr(tzinfo, "localize") - and hasattr(tzinfo, "zone") - and tzinfo.zone # type: ignore[attr-defined] + isinstance(tzinfo, dt_tzinfo) + and hasattr(tzinfo, "localize") + and hasattr(tzinfo, "zone") + and tzinfo.zone # type: ignore[attr-defined] ): tzinfo = parser.TzinfoParser.parse(tzinfo.zone) # type: ignore[attr-defined] elif isinstance(tzinfo, str): From e22f7b17f6fff2128a72a2ba8bb12dade08e4179 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 06/10] Hungarian Locale Update (#1123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Karsa Zoltán --- arrow/locales.py | 2 ++ tests/test_locales.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/arrow/locales.py b/arrow/locales.py index ef7a8edd..f0d4bc19 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -3709,6 +3709,8 @@ class HungarianLocale(Locale): "hours": {"past": "{0} órával", "future": "{0} óra"}, "day": {"past": "egy nappal", "future": "egy nap"}, "days": {"past": "{0} nappal", "future": "{0} nap"}, + "week": {"past": "egy héttel", "future": "egy hét"}, + "weeks": {"past": "{0} héttel", "future": "{0} hét"}, "month": {"past": "egy hónappal", "future": "egy hónap"}, "months": {"past": "{0} hónappal", "future": "{0} hónap"}, "year": {"past": "egy évvel", "future": "egy év"}, diff --git a/tests/test_locales.py b/tests/test_locales.py index 099f6f67..bef91d74 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -1269,6 +1269,12 @@ def test_format_timeframe(self): assert self.locale._format_timeframe("days", -2) == "2 nappal" assert self.locale._format_timeframe("days", 2) == "2 nap" + # Week(s) + assert self.locale._format_timeframe("week", -1) == "egy héttel" + assert self.locale._format_timeframe("week", 1) == "egy hét" + assert self.locale._format_timeframe("weeks", -2) == "2 héttel" + assert self.locale._format_timeframe("weeks", 2) == "2 hét" + # Month(s) assert self.locale._format_timeframe("month", -1) == "egy hónappal" assert self.locale._format_timeframe("month", 1) == "egy hónap" From 1b89ed58e1c8c51f3d0fc996398fd8987e9298e7 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 07/10] Parser: Allow UTC prefix in TzInfoParser (#1099) --- arrow/parser.py | 2 +- tests/test_parser.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/arrow/parser.py b/arrow/parser.py index e95d78b0..6bf2fba2 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -740,7 +740,7 @@ def _generate_choice_re( class TzinfoParser: _TZINFO_RE: ClassVar[Pattern[str]] = re.compile( - r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$" + r"^(?:\(UTC)*([\+\-])?(\d{2})(?:\:?(\d{2}))?" ) @classmethod diff --git a/tests/test_parser.py b/tests/test_parser.py index bb4ab148..e92d30c1 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1367,6 +1367,14 @@ def test_parse_utc(self): assert self.parser.parse("utc") == tz.tzutc() assert self.parser.parse("UTC") == tz.tzutc() + def test_parse_utc_withoffset(self): + assert self.parser.parse("(UTC+01:00") == tz.tzoffset(None, 3600) + assert self.parser.parse("(UTC-01:00") == tz.tzoffset(None, -3600) + assert self.parser.parse("(UTC+01:00") == tz.tzoffset(None, 3600) + assert self.parser.parse( + "(UTC+01:00) Amsterdam, Berlin, Bern, Rom, Stockholm, Wien" + ) == tz.tzoffset(None, 3600) + def test_parse_iso(self): assert self.parser.parse("01:00") == tz.tzoffset(None, 3600) From 608ec39478332188faef2dd3cea893b1b410d05f Mon Sep 17 00:00:00 2001 From: David Melbye Wechsler Date: Sat, 3 Sep 2022 21:22:53 +0200 Subject: [PATCH 08/10] fix(1090/default_tz): Fixed lint error - #1090. --- arrow/arrow.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index 3a5bb0f9..8009d5cc 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -170,17 +170,17 @@ def __init__( ) -> None: self.default_tz = default_tz self.default_tz_used = default_tz_used - # If default_tz_used is already set, tzinfo should also already be set - if not default_tz_used: + # If tzinfo is not a datetime.tzinfo object parse tzinfo + # Also detects if tzinfo is a pytz object (issue #626) + if not isinstance(tzinfo, dt_tzinfo) or ( + hasattr(tzinfo, "localize") + and hasattr(tzinfo, "zone") + and tzinfo.zone # type: ignore[attr-defined] + ): tzinfo, self.default_tz_used = util.get_tzinfo_default_used( default_tz=self.default_tz, tzinfo=tzinfo ) - # Cast due to mypy error - # Argument 8 to "datetime" has incompatible type - # "Union[tzinfo, str, None]"; expected "Optional[tzinfo]" - cast(dt_tzinfo, tzinfo) - fold = kwargs.get("fold", 0) self._datetime = dt_datetime( From 06dd56b10b77cd99daab6aca617e23f52c1af6c3 Mon Sep 17 00:00:00 2001 From: David Melbye Wechsler Date: Sun, 4 Sep 2022 14:29:33 +0200 Subject: [PATCH 09/10] fix(1090/default_tz): Fixed isort and black errors - #1090. --- arrow/api.py | 7 ++++--- arrow/arrow.py | 16 +++++++--------- arrow/util.py | 19 ++++++++++--------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/arrow/api.py b/arrow/api.py index fdb072cc..8d6e0509 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -4,11 +4,12 @@ """ -from datetime import date, datetime, tzinfo as dt_tzinfo +from datetime import date, datetime +from datetime import tzinfo as dt_tzinfo from time import struct_time -from typing import Any, List, Optional, overload, Tuple, Type, Union +from typing import Any, List, Optional, Tuple, Type, Union, overload -from arrow.arrow import Arrow, TZ_EXPR +from arrow.arrow import TZ_EXPR, Arrow from arrow.constants import DEFAULT_LOCALE from arrow.factory import ArrowFactory diff --git a/arrow/arrow.py b/arrow/arrow.py index 8009d5cc..6db278c9 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -8,27 +8,25 @@ import calendar import re import sys -from datetime import ( - date, - datetime as dt_datetime, - time as dt_time, - timedelta, - tzinfo as dt_tzinfo, -) +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, - cast, ClassVar, Generator, Iterable, List, Mapping, Optional, - overload, Tuple, Union, + cast, + overload, ) from dateutil import tz as dateutil_tz diff --git a/arrow/util.py b/arrow/util.py index 20d994ee..084bc780 100644 --- a/arrow/util.py +++ b/arrow/util.py @@ -1,8 +1,9 @@ """Helpful functions used internally within arrow.""" import datetime -from datetime import tzinfo as dt_tzinfo, datetime as dt_datetime -from typing import Any, Optional, cast, Tuple, Union +from datetime import datetime as dt_datetime +from datetime import tzinfo as dt_tzinfo +from typing import Any, Optional, Tuple, Union, cast from dateutil.rrule import WEEKLY, rrule @@ -120,9 +121,9 @@ def validate_bounds(bounds: str) -> None: def get_tzinfo_default_used( - default_tz: Union[dt_tzinfo, str], - dt: Optional[dt_datetime] = None, - tzinfo: Optional[Union[dt_tzinfo, str]] = None + default_tz: Union[dt_tzinfo, str], + dt: Optional[dt_datetime] = None, + tzinfo: Optional[Union[dt_tzinfo, str]] = None, ) -> Tuple[dt_tzinfo, bool]: """Get the tzinfo to use, and indicate if it was based on ``default_tz``. @@ -143,10 +144,10 @@ def get_tzinfo_default_used( # detect that tzinfo is a pytz object (issue #626) if ( - isinstance(tzinfo, dt_tzinfo) - and hasattr(tzinfo, "localize") - and hasattr(tzinfo, "zone") - and tzinfo.zone # type: ignore[attr-defined] + isinstance(tzinfo, dt_tzinfo) + and hasattr(tzinfo, "localize") + and hasattr(tzinfo, "zone") + and tzinfo.zone # type: ignore[attr-defined] ): tzinfo = parser.TzinfoParser.parse(tzinfo.zone) # type: ignore[attr-defined] elif isinstance(tzinfo, str): From 680fd9f87f02815f40619cb887ef7b0d9e6b3a7e Mon Sep 17 00:00:00 2001 From: David Melbye Wechsler Date: Tue, 6 Sep 2022 17:55:43 +0200 Subject: [PATCH 10/10] fix(1090/default_tz): Fixed docstring in ArrowFactory.get - #1090. --- arrow/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow/factory.py b/arrow/factory.py index 7c87f205..ba04a272 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -96,7 +96,7 @@ def get( def get(self, *args: Any, **kwargs: Any) -> Arrow: """Returns an :class:`Arrow ` object based on flexible inputs. - :param locale: (optional) a ``s + :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.