Repository: celery/celery
File: celery/utils/time.py (Python)

Code before:
    """Either ``datetime | str -> datetime`` or ``None -> None``."""
    if not dt:
        return
    if isinstance(dt, datetime):
        return dt
    return datetime.fromisoformat(dt)


def is_naive(dt: datetime) -> bool:
    """Return True if :class:`~datetime.datetime` is naive, meaning it doesn't have timezone info set."""
    return dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None


def _can_detect_ambiguous(tz: tzinfo) -> bool:
    """Helper function to determine if a timezone can detect ambiguous times using dateutil."""

    return isinstance(tz, ZoneInfo) or hasattr(tz, "is_ambiguous")


def _is_ambigious(dt: datetime, tz: tzinfo) -> bool:
    """Helper function to determine if a timezone is ambigious using dateutil.

    Returns False if the timezone cannot detect ambiguity, or if there is no abiguity, otherwise True.

    In order to detect ambiguous datetimes, the timezone must be built using ZoneInfo, or have an is_ambiguous
    method. Previously, pytz timezones would throw an AmbiguousTimeError if the localized dt was ambigious,
    but now we need to specifically check for ambiguity with dateutil, as pytz is deprecated.
    """

    return _can_detect_ambiguous(tz) and dateutil_tz.datetime_ambiguous(dt)


def make_aware(dt: datetime, tz: tzinfo) -> datetime:
    """Set timezone for a :class:`~datetime.datetime` object."""

    dt = dt.replace(tzinfo=tz)
    if _is_ambigious(dt, tz):
        dt = min(dt.replace(fold=0), dt.replace(fold=1))
    return dt


def localize(dt: datetime, tz: tzinfo) -> datetime:
    """Convert aware :class:`~datetime.datetime` to another timezone.

    Using a ZoneInfo timezone will give the most flexibility in terms of ambiguous DST handling.
    """
    if is_naive(dt):  # Ensure timezone aware datetime
        dt = make_aware(dt, tz)
    if dt.tzinfo == ZoneInfo("UTC"):
        dt = dt.astimezone(tz)  # Always safe to call astimezone on utc zones
    return dt

Review comment (suggestion):
```suggestion
    method. Previously, pytz timezones would throw an AmbiguousTimeError if the localized dt was ambiguous,
```

Code after:
    """Either ``datetime | str -> datetime`` or ``None -> None``."""
    if not dt:
        return
    if isinstance(dt, datetime):
        return dt
    return datetime.fromisoformat(dt)


def is_naive(dt: datetime) -> bool:
    """Return True if :class:`~datetime.datetime` is naive, meaning it doesn't have timezone info set."""
    return dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None


def _can_detect_ambiguous(tz: tzinfo) -> bool:
    """Helper function to determine if a timezone can detect ambiguous times using dateutil."""

    return isinstance(tz, ZoneInfo) or hasattr(tz, "is_ambiguous")


def _is_ambigious(dt: datetime, tz: tzinfo) -> bool:
    """Helper function to determine if a timezone is ambiguous using python's dateutil module.

    Returns False if the timezone cannot detect ambiguity, or if there is no ambiguity, otherwise True.

    In order to detect ambiguous datetimes, the timezone must be built using ZoneInfo, or have an is_ambiguous
    method. Previously, pytz timezones would throw an AmbiguousTimeError if the localized dt was ambiguous,
    but now we need to specifically check for ambiguity with dateutil, as pytz is deprecated.
    """

    return _can_detect_ambiguous(tz) and dateutil_tz.datetime_ambiguous(dt)


def make_aware(dt: datetime, tz: tzinfo) -> datetime:
    """Set timezone for a :class:`~datetime.datetime` object."""

    dt = dt.replace(tzinfo=tz)
    if _is_ambigious(dt, tz):
        dt = min(dt.replace(fold=0), dt.replace(fold=1))
    return dt


def localize(dt: datetime, tz: tzinfo) -> datetime:
    """Convert aware :class:`~datetime.datetime` to another timezone.

    Using a ZoneInfo timezone will give the most flexibility in terms of ambiguous DST handling.
    """
    if is_naive(dt):  # Ensure timezone aware datetime
        dt = make_aware(dt, tz)
    if dt.tzinfo == ZoneInfo("UTC"):
        dt = dt.astimezone(tz)  # Always safe to call astimezone on utc zones
    return dt