File size: 7,972 Bytes
bbae060
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# -*- coding: utf-8 -*-
"""
The ``dateutil.zoneinfo`` module was originally designed to work with a tarball
shipped with ``dateutil``. Since ``dateutil`` now uses data from the same
sources as :py:mod:`zoneinfo`, this module is no longer necessary.

.. caution::
    This module is deprecated, and its functionality has been dramatically
    curtailed. It is recommended that you adopt the standard library module
    :py:mod:`zoneinfo` or :func:`dateutil.tz.gettz` instead.

"""

import json
import sys
import warnings
from tarfile import TarFile

import six

from dateutil import _tzdata_impl
from dateutil.tz import tzfile as _tzfile

__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]

ZONEFILENAME = None
METADATA_FN = 'METADATA'


def _importer_stacklevel():
    """The ``stacklevel`` that attributes a warning to the importing code.

    This module can be imported through ``dateutil.__getattr__`` (e.g. by
    ``from dateutil import zoneinfo``), which calls
    ``importlib.import_module``; the warnings machinery skips the import
    system's own frames, but not those two, so skip them here.
    """
    level = 1
    frame = sys._getframe(1)  # The body of this module
    while frame.f_back is not None:
        frame = frame.f_back
        filename = frame.f_code.co_filename
        if "importlib" in filename and "_bootstrap" in filename:
            # Since Python 3.5, the warnings machinery skips these frames
            # without counting them; before that they count like any other.
            if sys.version_info < (3, 5):
                level += 1
            continue

        level += 1
        if frame.f_globals.get("__name__") not in ("importlib", "dateutil"):
            return level

    return 2


warnings.warn(
    "The `dateutil.zoneinfo` module has been replaced with a wrapper around "
    "the tzdata package, and its use is deprecated, to be removed in a future "
    "version. Use the standard library module `zoneinfo` or `dateutil.tz` "
    "instead.",
    DeprecationWarning,
    stacklevel=_importer_stacklevel(),
)


tzfile = _tzfile

class ZoneInfoFile(object):
    def __init__(self, zonefile_stream=None):
        if zonefile_stream is not None:
            self._load_legacy_zonefile_stream(zonefile_stream)
        else:
            self._eager_load_tzdata()

    def _eager_load_tzdata(self):
        try:
            import tzdata
        except ImportError as e:
            six.raise_from(
                ImportError(
                    "The tzdata module is required to use ZoneInfoFile; either add "
                    "a dependency on tzdata or migrate away from dateutil.zoneinfo."
                ),
                e,
            )

        zone_dict = {}

        for key in _tzdata_impl._load_tzdata_keys():
            with _tzdata_impl._load_tzdata(key) as f:
                zone_dict[key] = tzfile(f, key=key)

        self.zones = zone_dict
        self.metadata = {
            "metadata_version": "3.0",
            "tzversion": tzdata.IANA_VERSION,
        }

    def _load_legacy_zonefile_stream(self, zonefile_stream):
        with TarFile.open(fileobj=zonefile_stream) as tf:
            self.zones = {
                zf.name: tzfile(tf.extractfile(zf), filename=zf.name)
                for zf in tf.getmembers()
                if zf.isfile() and zf.name != METADATA_FN
            }
            # deal with links: They'll point to their parent object. Less
            # waste of memory
            links = {
                zl.name: self.zones[zl.linkname]
                for zl in tf.getmembers()
                if zl.islnk() or zl.issym()
            }
            self.zones.update(links)
            try:
                metadata_json = tf.extractfile(tf.getmember(METADATA_FN))
                metadata_str = metadata_json.read().decode("UTF-8")
                self.metadata = json.loads(metadata_str)
            except KeyError:
                # no metadata in tar file
                self.metadata = None

    def get(self, name, default=None):
        """
        Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method
        for retrieving zones from the zone dictionary.

        :param name:
            The name of the zone to retrieve. (Generally IANA zone names)

        :param default:
            The value to return in the event of a missing key.

        .. versionadded:: 2.6.0

        """
        return self.zones.get(name, default)


# The current API has gettz as a module function, although in fact it taps into
# a stateful class. So as a workaround for now, without changing the API, we
# will create a new "global" class instance the first time a user requests a
# timezone. Ugly, but adheres to the api.
#
# TODO: Remove after deprecation period.
_CLASS_ZONE_INSTANCE = []


def get_zonefile_instance(new_instance=False):
    """
    This is a convenience function which provides a :class:`ZoneInfoFile`
    instance using the data provided by the ``dateutil`` package. By default, it
    caches a single instance of the ZoneInfoFile object and returns that.

    :param new_instance:
        If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and
        used as the cached instance for the next call. Otherwise, new instances
        are created only as necessary.

    :return:
        Returns a :class:`ZoneInfoFile` object.

    .. versionadded:: 2.6
    """
    if new_instance:
        zif = None
    else:
        zif = getattr(get_zonefile_instance, '_cached_instance', None)

    if zif is None:
        zif = ZoneInfoFile()

        get_zonefile_instance._cached_instance = zif

    return zif


def gettz(name):
    """
    This retrieves a time zone from the ``tzdata`` package.

    :param name:
        An IANA-style time zone name, as found in the zoneinfo file.

    :return:
        Returns a :class:`dateutil.tz.tzfile` time zone object.

    .. warning::
        It is generally inadvisable to use this function, and it is only
        provided for API compatibility with earlier versions. This is *not*
        equivalent to ``dateutil.tz.gettz()``, which selects an appropriate
        time zone based on the inputs, favoring system zoneinfo. This is ONLY
        for accessing the dateutil-specific zoneinfo (which may be out of
        date compared to the system zoneinfo).

    .. deprecated:: 2.6
        If you need to use a specific zoneinfofile over the system zoneinfo,
        instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call
        :func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead.

        Use :func:`get_zonefile_instance` to retrieve an instance of the
        dateutil-provided zoneinfo.
    """
    warnings.warn("zoneinfo.gettz() will be removed in future versions, "
                  "to use the dateutil-provided zoneinfo files, instantiate a "
                  "ZoneInfoFile object and use ZoneInfoFile.zones.get() "
                  "instead. See the documentation for details.",
                  DeprecationWarning)

    if len(_CLASS_ZONE_INSTANCE) == 0:
        _CLASS_ZONE_INSTANCE.append(ZoneInfoFile())
    return _CLASS_ZONE_INSTANCE[0].zones.get(name)


def gettz_db_metadata():
    """Get the zonefile metadata

    :returns:
        A dictionary with the database metadata

    .. deprecated:: 2.6
        See deprecation warning in :func:`zoneinfo.gettz`. To get metadata,
        query the attribute ``zoneinfo.ZoneInfoFile.metadata``.
    """
    warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future "
                  "versions, to use the dateutil-provided zoneinfo files, "
                  "ZoneInfoFile object and query the 'metadata' attribute "
                  "instead. See the documentation for details.",
                  DeprecationWarning)

    if len(_CLASS_ZONE_INSTANCE) == 0:
        _CLASS_ZONE_INSTANCE.append(ZoneInfoFile())
    return _CLASS_ZONE_INSTANCE[0].metadata