diff --git a/arrow/arrow.py b/arrow/arrow.py index eecf2326..d6168624 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -321,7 +321,27 @@ def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arr tzinfo = timezone.utc else: tzinfo = dt.tzinfo - + fold = getattr(dt, "fold", 0) + else: + # When replacing tzinfo, infer the correct fold by comparing + # the original UTC offset to what fold=1 produces in the new tz. + # This correctly handles ambiguous times (e.g. DST fall-back) issue #1162. + fold = getattr(dt, "fold", 0) + if dt.tzinfo is not None and dt.utcoffset() is not None: + resolved = cls._get_tzinfo(tzinfo) + dt_fold1 = dt_datetime( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=resolved, + fold=1, + ) + if dt_fold1.utcoffset() == dt.utcoffset(): + fold = 1 return cls( dt.year, dt.month, @@ -331,7 +351,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 diff --git a/arrow/factory.py b/arrow/factory.py index 0913bfe1..57b90243 100644 --- a/arrow/factory.py +++ b/arrow/factory.py @@ -245,6 +245,11 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow: # (str) -> parse @ tzinfo elif isinstance(arg, str): dt = parser.DateTimeParser(locale).parse_iso(arg, normalize_whitespace) + if tz is not None and dt.tzinfo is not None: + # If the parsed string already has offset info, convert to + # the target timezone (preserving the UTC moment) rather than + # replacing the tzinfo, so ambiguous times are resolved correctly issue #1162. + return self.type.fromdatetime(dt).to(tz) return self.type.fromdatetime(dt, tzinfo=tz) # (struct_time) -> from struct_time diff --git a/tests/test_arrow.py b/tests/test_arrow.py index b595e4e2..6b29c948 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -1141,6 +1141,21 @@ def test_range_over_year_maintains_end_date_across_leap_year(self): arrow.Arrow(2016, 2, 29), ] + def test_range_dst_fall_back_includes_repeated_hour(self): + # Issue #1162: When iterating over a DST fall-back period, the repeated + # hour should not be skipped. A string with an explicit UTC offset and a + # named tzinfo kwarg should be converted (not replaced), preserving the + # correct UTC moment and fold. + import arrow as arrow_module + + dts = arrow_module.get("2021-11-07T01:00:00-05:00", tzinfo="US/Eastern") + dte = arrow_module.get("2021-11-07T02:00:00-06:00", tzinfo="US/Eastern") + result = list(arrow.Arrow.range("hours", dts, dte)) + assert len(result) == 3 + assert result[0].utcoffset().total_seconds() == -5 * 3600 + assert result[1].utcoffset().total_seconds() == -5 * 3600 + assert result[2].utcoffset().total_seconds() == -5 * 3600 + class TestArrowSpanRange: def test_year(self):