diff --git a/arrow/api.py b/arrow/api.py index d8ed24b9..35bb4249 100644 --- a/arrow/api.py +++ b/arrow/api.py @@ -25,6 +25,7 @@ def get( *, locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, + fold: Optional[int] = 0, normalize_whitespace: bool = False, ) -> Arrow: ... # pragma: no cover @@ -35,6 +36,7 @@ def get( *args: int, locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, + fold: Optional[int] = 0, normalize_whitespace: bool = False, ) -> Arrow: ... # pragma: no cover @@ -56,6 +58,7 @@ def get( *, locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, + fold: Optional[int] = 0, normalize_whitespace: bool = False, ) -> Arrow: ... # pragma: no cover @@ -68,6 +71,7 @@ def get( *, locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, + fold: Optional[int] = 0, normalize_whitespace: bool = False, ) -> Arrow: ... # pragma: no cover @@ -80,6 +84,7 @@ def get( *, locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, + fold: Optional[int] = 0, normalize_whitespace: bool = False, ) -> Arrow: ... # pragma: no cover diff --git a/arrow/arrow.py b/arrow/arrow.py index e855eee0..d88ec91b 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -159,7 +159,8 @@ def __init__( second: int = 0, microsecond: int = 0, tzinfo: Optional[TZ_EXPR] = None, - **kwargs: Any, + *, + fold: int = 0, ) -> None: if tzinfo is None: tzinfo = dateutil_tz.tzutc() @@ -174,8 +175,6 @@ def __init__( elif isinstance(tzinfo, str): tzinfo = parser.TzinfoParser.parse(tzinfo) - fold = kwargs.get("fold", 0) - self._datetime = dt_datetime( year, month, day, hour, minute, second, microsecond, tzinfo, fold=fold ) @@ -210,7 +209,7 @@ def now(cls, tzinfo: Optional[dt_tzinfo] = None) -> "Arrow": dt.second, dt.microsecond, dt.tzinfo, - fold=getattr(dt, "fold", 0), + fold=dt.fold, ) @classmethod @@ -236,7 +235,7 @@ def utcnow(cls) -> "Arrow": dt.second, dt.microsecond, dt.tzinfo, - fold=getattr(dt, "fold", 0), + fold=dt.fold, ) @classmethod @@ -273,7 +272,7 @@ def fromtimestamp( dt.second, dt.microsecond, dt.tzinfo, - fold=getattr(dt, "fold", 0), + fold=dt.fold, ) @classmethod @@ -299,11 +298,16 @@ def utcfromtimestamp(cls, timestamp: Union[int, float, str]) -> "Arrow": dt.second, dt.microsecond, dateutil_tz.tzutc(), - fold=getattr(dt, "fold", 0), + fold=dt.fold, ) @classmethod - def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": + def fromdatetime( + cls, + dt: dt_datetime, + tzinfo: Optional[TZ_EXPR] = None, + fold: Optional[int] = None, + ) -> "Arrow": """Constructs an :class:`Arrow ` object from a ``datetime`` and optional replacement timezone. @@ -326,6 +330,9 @@ def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arr else: tzinfo = dt.tzinfo + if fold is None: + fold = dt.fold + return cls( dt.year, dt.month, @@ -335,7 +342,7 @@ def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arr dt.second, dt.microsecond, tzinfo, - fold=getattr(dt, "fold", 0), + fold=fold, ) @classmethod @@ -355,10 +362,14 @@ def fromdate(cls, date: date, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": @classmethod def strptime( - cls, date_str: str, fmt: str, tzinfo: Optional[TZ_EXPR] = None + cls, + date_str: str, + fmt: str, + tzinfo: Optional[TZ_EXPR] = None, + fold: Optional[int] = None, ) -> "Arrow": """Constructs an :class:`Arrow ` object from a date string and format, - in the style of ``datetime.strptime``. Optionally replaces the parsed timezone. + in the style of ``datetime.strptime``. Optionally replaces the parsed timezone and fold. :param date_str: the date string. :param fmt: the format string using datetime format codes. @@ -376,6 +387,9 @@ def strptime( if tzinfo is None: tzinfo = dt.tzinfo + if fold is None: + fold = dt.fold + return cls( dt.year, dt.month, @@ -385,7 +399,7 @@ def strptime( dt.second, dt.microsecond, tzinfo, - fold=getattr(dt, "fold", 0), + fold=fold, ) @classmethod @@ -413,7 +427,7 @@ def fromordinal(cls, ordinal: int) -> "Arrow": dt.second, dt.microsecond, dt.tzinfo, - fold=getattr(dt, "fold", 0), + fold=dt.fold, ) # factories: ranges and spans @@ -1087,7 +1101,7 @@ def to(self, tz: TZ_EXPR) -> "Arrow": dt.second, dt.microsecond, dt.tzinfo, - fold=getattr(dt, "fold", 0), + fold=dt.fold, ) # string output and formatting diff --git a/arrow/factory.py b/arrow/factory.py index aad4af8b..f8bedb25 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -40,6 +40,7 @@ def get( *, locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, + fold: Optional[int] = None, normalize_whitespace: bool = False, ) -> Arrow: ... # pragma: no cover @@ -61,6 +62,7 @@ def get( *, locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, + fold: Optional[int] = None, normalize_whitespace: bool = False, ) -> Arrow: ... # pragma: no cover @@ -73,6 +75,7 @@ def get( *, locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, + fold: Optional[int] = None, normalize_whitespace: bool = False, ) -> Arrow: ... # pragma: no cover @@ -85,6 +88,7 @@ def get( *, locale: str = DEFAULT_LOCALE, tzinfo: Optional[TZ_EXPR] = None, + fold: Optional[int] = None, normalize_whitespace: bool = False, ) -> Arrow: ... # pragma: no cover @@ -96,6 +100,11 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: :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. + :param fold: (optional) an ``int`` value of 0 or 1. + Replaces the fold value, used to disambiguate repeated wall times. + Used only when the first argument is an Arrow instance/datetime/datetime string, + or datetime constructor kwargs were provided. + :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. @@ -196,14 +205,19 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: arg_count = len(args) locale = kwargs.pop("locale", DEFAULT_LOCALE) tz = kwargs.get("tzinfo", None) + fold = kwargs.get("fold") normalize_whitespace = kwargs.pop("normalize_whitespace", False) - # if kwargs given, send to constructor unless only tzinfo provided - if len(kwargs) > 1: + # if kwargs given, send to constructor unless only tzinfo and/or fold provided + if len(kwargs) > 2: + arg_count = 3 + + # either tzinfo or fold kwarg is not provided + elif len(kwargs) == 2 and None in (tz, fold): arg_count = 3 - # tzinfo kwarg is not provided - if len(kwargs) == 1 and tz is None: + # tzinfo and fold kwargs are both not provided + elif len(kwargs) == 1 and tz is fold is None: arg_count = 3 # () -> now, @ tzinfo or utc @@ -235,11 +249,11 @@ 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, tzinfo=tz) + return self.type.fromdatetime(arg.datetime, tzinfo=tz, fold=fold) # (datetime) -> from datetime @ tzinfo elif isinstance(arg, datetime): - return self.type.fromdatetime(arg, tzinfo=tz) + return self.type.fromdatetime(arg, tzinfo=tz, fold=fold) # (date) -> from date @ tzinfo elif isinstance(arg, date): @@ -252,7 +266,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, tzinfo=tz, fold=fold) # (struct_time) -> from struct_time elif isinstance(arg, struct_time): @@ -274,7 +288,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, tzinfo=arg_2, fold=fold) else: raise TypeError( f"Cannot parse two arguments of types 'datetime', {type(arg_2)!r}." @@ -295,7 +309,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, tzinfo=tz, fold=fold) else: raise TypeError( diff --git a/tests/test_arrow.py b/tests/test_arrow.py index 5cd12c82..f7efee4a 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -74,9 +74,6 @@ 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) - assert hasattr(before, "fold") - assert hasattr(after, "fold") - # PEP-495 requires the comparisons below to be true assert before == after assert before.utcoffset() != after.utcoffset() @@ -183,6 +180,14 @@ def test_strptime(self): 2013, 2, 3, 12, 30, 45, tzinfo=tz.gettz("Europe/Paris") ) + def test_strptime_with_fold(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", fold=1) + assert result._datetime == datetime(2013, 2, 3, 12, 30, 45, tzinfo=tz.tzutc()) + assert result.fold == 1 + def test_fromordinal(self): timestamp = 1607066909.937968 diff --git a/tests/test_factory.py b/tests/test_factory.py index f368126c..97774eaa 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -97,12 +97,31 @@ def test_one_arg_arrow(self): assert arw == result + def test_one_arg_arrow_with_fold(self): + + arw = self.factory.utcnow() + result = self.factory.get(arw, fold=1) + + # fold is ignored for comparison + assert arw.fold == 0 + assert result.fold == 1 + 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_datetime_with_fold(self): + + dt = datetime.utcnow().replace(tzinfo=tz.tzutc()) + result = self.factory.get(dt, fold=1) + + assert dt.fold == 0 + assert result.fold == 1 + assert result == dt + def test_one_arg_date(self): d = date.today()