bazarr/libs/pytz_deprecation_shim/_impl.py

297 lines
9.2 KiB
Python

# -*- coding: utf-8 -*-
import warnings
from datetime import tzinfo
from . import _compat
from ._exceptions import (
AmbiguousTimeError,
NonExistentTimeError,
PytzUsageWarning,
UnknownTimeZoneError,
get_exception,
)
IS_DST_SENTINEL = object()
KEY_SENTINEL = object()
def timezone(key, _cache={}):
"""Builds an IANA database time zone shim.
This is the equivalent of ``pytz.timezone``.
:param key:
A valid key from the IANA time zone database.
:raises UnknownTimeZoneError:
If an unknown value is passed, this will raise an exception that can be
caught by :exc:`pytz_deprecation_shim.UnknownTimeZoneError` or
``pytz.UnknownTimeZoneError``. Like
:exc:`zoneinfo.ZoneInfoNotFoundError`, both of those are subclasses of
:exc:`KeyError`.
"""
instance = _cache.get(key, None)
if instance is None:
if len(key) == 3 and key.lower() == "utc":
instance = _cache.setdefault(key, UTC)
else:
try:
zone = _compat.get_timezone(key)
except KeyError:
raise get_exception(UnknownTimeZoneError, key)
instance = _cache.setdefault(key, wrap_zone(zone, key=key))
return instance
def fixed_offset_timezone(offset, _cache={}):
"""Builds a fixed offset time zone shim.
This is the equivalent of ``pytz.FixedOffset``. An alias is available as
``pytz_deprecation_shim.FixedOffset`` as well.
:param offset:
A fixed offset from UTC, in minutes. This must be in the range ``-1439
<= offset <= 1439``.
:raises ValueError:
For offsets whose absolute value is greater than or equal to 24 hours.
:return:
A shim time zone.
"""
if not (-1440 < offset < 1440):
raise ValueError("absolute offset is too large", offset)
instance = _cache.get(offset, None)
if instance is None:
if offset == 0:
instance = _cache.setdefault(offset, UTC)
else:
zone = _compat.get_fixed_offset_zone(offset)
instance = _cache.setdefault(offset, wrap_zone(zone, key=None))
return instance
def build_tzinfo(zone, fp):
"""Builds a shim object from a TZif file.
This is a shim for ``pytz.build_tzinfo``. Given a value to use as the zone
IANA key and a file-like object containing a valid TZif file (i.e.
conforming to :rfc:`8536`), this builds a time zone object and wraps it in
a shim class.
The argument names are chosen to match those in ``pytz.build_tzinfo``.
:param zone:
A string to be used as the time zone object's IANA key.
:param fp:
A readable file-like object emitting bytes, pointing to a valid TZif
file.
:return:
A shim time zone.
"""
zone_file = _compat.get_timezone_file(fp)
return wrap_zone(zone_file, key=zone)
def wrap_zone(tz, key=KEY_SENTINEL, _cache={}):
"""Wrap an existing time zone object in a shim class.
This is likely to be useful if you would like to work internally with
non-``pytz`` zones, but you expose an interface to callers relying on
``pytz``'s interface. It may also be useful for passing non-``pytz`` zones
to libraries expecting to use ``pytz``'s interface.
:param tz:
A :pep:`495`-compatible time zone, such as those provided by
:mod:`dateutil.tz` or :mod:`zoneinfo`.
:param key:
The value for the IANA time zone key. This is optional for ``zoneinfo``
zones, but required for ``dateutil.tz`` zones.
:return:
A shim time zone.
"""
if key is KEY_SENTINEL:
key = getattr(tz, "key", KEY_SENTINEL)
if key is KEY_SENTINEL:
raise TypeError(
"The `key` argument is required when wrapping zones that do not "
+ "have a `key` attribute."
)
instance = _cache.get((id(tz), key), None)
if instance is None:
instance = _cache.setdefault((id(tz), key), _PytzShimTimezone(tz, key))
return instance
class _PytzShimTimezone(tzinfo):
# Add instance variables for _zone and _key because this will make error
# reporting with partially-initialized _BasePytzShimTimezone objects
# work better.
_zone = None
_key = None
def __init__(self, zone, key):
self._key = key
self._zone = zone
def utcoffset(self, dt):
return self._zone.utcoffset(dt)
def dst(self, dt):
return self._zone.dst(dt)
def tzname(self, dt):
return self._zone.tzname(dt)
def fromutc(self, dt):
# The default fromutc implementation only works if tzinfo is "self"
dt_base = dt.replace(tzinfo=self._zone)
dt_out = self._zone.fromutc(dt_base)
return dt_out.replace(tzinfo=self)
def __str__(self):
if self._key is not None:
return str(self._key)
else:
return repr(self)
def __repr__(self):
return "%s(%s, %s)" % (
self.__class__.__name__,
repr(self._zone),
repr(self._key),
)
def unwrap_shim(self):
"""Returns the underlying class that the shim is a wrapper for.
This is a shim-specific method equivalent to
:func:`pytz_deprecation_shim.helpers.upgrade_tzinfo`. It is provided as
a method to allow end-users to upgrade shim timezones without requiring
an explicit dependency on ``pytz_deprecation_shim``, e.g.:
.. code-block:: python
if getattr(tz, "unwrap_shim", None) is None:
tz = tz.unwrap_shim()
"""
return self._zone
@property
def zone(self):
warnings.warn(
"The zone attribute is specific to pytz's interface; "
+ "please migrate to a new time zone provider. "
+ "For more details on how to do so, see %s"
% PYTZ_MIGRATION_GUIDE_URL,
PytzUsageWarning,
stacklevel=2,
)
return self._key
def localize(self, dt, is_dst=IS_DST_SENTINEL):
warnings.warn(
"The localize method is no longer necessary, as this "
+ "time zone supports the fold attribute (PEP 495). "
+ "For more details on migrating to a PEP 495-compliant "
+ "implementation, see %s" % PYTZ_MIGRATION_GUIDE_URL,
PytzUsageWarning,
stacklevel=2,
)
if dt.tzinfo is not None:
raise ValueError("Not naive datetime (tzinfo is already set)")
dt_out = dt.replace(tzinfo=self)
if is_dst is IS_DST_SENTINEL:
return dt_out
dt_ambiguous = _compat.is_ambiguous(dt_out)
dt_imaginary = (
_compat.is_imaginary(dt_out) if not dt_ambiguous else False
)
if is_dst is None:
if dt_imaginary:
raise get_exception(
NonExistentTimeError, dt.replace(tzinfo=None)
)
if dt_ambiguous:
raise get_exception(AmbiguousTimeError, dt.replace(tzinfo=None))
elif dt_ambiguous or dt_imaginary:
# Start by normalizing the folds; dt_out may have fold=0 or fold=1,
# but we need to know the DST offset on both sides anyway, so we
# will get one datetime representing each side of the fold, then
# decide which one we're going to return.
if _compat.get_fold(dt_out):
dt_enfolded = dt_out
dt_out = _compat.enfold(dt_out, fold=0)
else:
dt_enfolded = _compat.enfold(dt_out, fold=1)
# Now we want to decide whether the fold=0 or fold=1 represents
# what pytz would return for `is_dst=True`
enfolded_dst = bool(dt_enfolded.dst())
if bool(dt_out.dst()) == enfolded_dst:
# If this is not a transition between standard time and
# daylight saving time, pytz will consider the larger offset
# the DST offset.
enfolded_dst = dt_enfolded.utcoffset() > dt_out.utcoffset()
# The default we've established is that dt_out is fold=0; swap it
# for the fold=1 datetime if is_dst == True and the enfolded side
# is DST or if is_dst == False and the enfolded side is *not* DST.
if is_dst == enfolded_dst:
dt_out = dt_enfolded
return dt_out
def normalize(self, dt):
warnings.warn(
"The normalize method is no longer necessary, as this "
+ "time zone supports the fold attribute (PEP 495). "
+ "For more details on migrating to a PEP 495-compliant "
+ "implementation, see %s" % PYTZ_MIGRATION_GUIDE_URL,
PytzUsageWarning,
stacklevel=2,
)
if dt.tzinfo is None:
raise ValueError("Naive time - no tzinfo set")
if dt.tzinfo is self:
return dt
return dt.astimezone(self)
def __copy__(self):
return self
def __deepcopy__(self, memo=None):
return self
def __reduce__(self):
return wrap_zone, (self._zone, self._key)
UTC = wrap_zone(_compat.UTC, "UTC")
PYTZ_MIGRATION_GUIDE_URL = (
"https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html"
)