python-dateutil 2.9.0__py2.py3-none-any.whl → 3.0.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dateutil/tz/tz.py CHANGED
@@ -7,24 +7,33 @@ etc), TZ environment string (in all known formats), given ranges (with help
7
7
  from relative deltas), local machine timezone, fixed offset timezone, and UTC
8
8
  timezone.
9
9
  """
10
+
11
+ import bisect
10
12
  import datetime
13
+ import os
11
14
  import struct
12
- import time
13
15
  import sys
14
- import os
15
- import bisect
16
+ import time
17
+ import warnings
16
18
  import weakref
17
19
  from collections import OrderedDict
18
20
 
19
21
  import six
20
22
  from six import string_types
21
23
  from six.moves import _thread
22
- from ._common import tzname_in_python2, _tzinfo
23
- from ._common import tzrangebase, enfold
24
- from ._common import _validate_fromutc_inputs
25
24
 
26
- from ._factories import _TzSingleton, _TzOffsetFactory
27
- from ._factories import _TzStrFactory
25
+ from .. import _tzdata_impl
26
+ from . import _tzpath
27
+ from ._common import (
28
+ _tzinfo,
29
+ _validate_fromutc_inputs,
30
+ enfold,
31
+ tzname_in_python2,
32
+ tzrangebase,
33
+ )
34
+ from ._factories import _TzOffsetFactory, _TzSingleton, _TzStrFactory
35
+ from ._tzfile import tzfile
36
+
28
37
  try:
29
38
  from .win import tzwin, tzwinlocal
30
39
  except ImportError:
@@ -35,7 +44,6 @@ from warnings import warn
35
44
 
36
45
  ZERO = datetime.timedelta(0)
37
46
  EPOCH = datetime.datetime(1970, 1, 1, 0, 0)
38
- EPOCHORDINAL = EPOCH.toordinal()
39
47
 
40
48
 
41
49
  @six.add_metaclass(_TzSingleton)
@@ -325,552 +333,6 @@ class tzlocal(_tzinfo):
325
333
  __reduce__ = object.__reduce__
326
334
 
327
335
 
328
- class _ttinfo(object):
329
- __slots__ = ["offset", "delta", "isdst", "abbr",
330
- "isstd", "isgmt", "dstoffset"]
331
-
332
- def __init__(self):
333
- for attr in self.__slots__:
334
- setattr(self, attr, None)
335
-
336
- def __repr__(self):
337
- l = []
338
- for attr in self.__slots__:
339
- value = getattr(self, attr)
340
- if value is not None:
341
- l.append("%s=%s" % (attr, repr(value)))
342
- return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
343
-
344
- def __eq__(self, other):
345
- if not isinstance(other, _ttinfo):
346
- return NotImplemented
347
-
348
- return (self.offset == other.offset and
349
- self.delta == other.delta and
350
- self.isdst == other.isdst and
351
- self.abbr == other.abbr and
352
- self.isstd == other.isstd and
353
- self.isgmt == other.isgmt and
354
- self.dstoffset == other.dstoffset)
355
-
356
- __hash__ = None
357
-
358
- def __ne__(self, other):
359
- return not (self == other)
360
-
361
- def __getstate__(self):
362
- state = {}
363
- for name in self.__slots__:
364
- state[name] = getattr(self, name, None)
365
- return state
366
-
367
- def __setstate__(self, state):
368
- for name in self.__slots__:
369
- if name in state:
370
- setattr(self, name, state[name])
371
-
372
-
373
- class _tzfile(object):
374
- """
375
- Lightweight class for holding the relevant transition and time zone
376
- information read from binary tzfiles.
377
- """
378
- attrs = ['trans_list', 'trans_list_utc', 'trans_idx', 'ttinfo_list',
379
- 'ttinfo_std', 'ttinfo_dst', 'ttinfo_before', 'ttinfo_first']
380
-
381
- def __init__(self, **kwargs):
382
- for attr in self.attrs:
383
- setattr(self, attr, kwargs.get(attr, None))
384
-
385
-
386
- class tzfile(_tzinfo):
387
- """
388
- This is a ``tzinfo`` subclass that allows one to use the ``tzfile(5)``
389
- format timezone files to extract current and historical zone information.
390
-
391
- :param fileobj:
392
- This can be an opened file stream or a file name that the time zone
393
- information can be read from.
394
-
395
- :param filename:
396
- This is an optional parameter specifying the source of the time zone
397
- information in the event that ``fileobj`` is a file object. If omitted
398
- and ``fileobj`` is a file stream, this parameter will be set either to
399
- ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``.
400
-
401
- See `Sources for Time Zone and Daylight Saving Time Data
402
- <https://data.iana.org/time-zones/tz-link.html>`_ for more information.
403
- Time zone files can be compiled from the `IANA Time Zone database files
404
- <https://www.iana.org/time-zones>`_ with the `zic time zone compiler
405
- <https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_
406
-
407
- .. note::
408
-
409
- Only construct a ``tzfile`` directly if you have a specific timezone
410
- file on disk that you want to read into a Python ``tzinfo`` object.
411
- If you want to get a ``tzfile`` representing a specific IANA zone,
412
- (e.g. ``'America/New_York'``), you should call
413
- :func:`dateutil.tz.gettz` with the zone identifier.
414
-
415
-
416
- **Examples:**
417
-
418
- Using the US Eastern time zone as an example, we can see that a ``tzfile``
419
- provides time zone information for the standard Daylight Saving offsets:
420
-
421
- .. testsetup:: tzfile
422
-
423
- from dateutil.tz import gettz
424
- from datetime import datetime
425
-
426
- .. doctest:: tzfile
427
-
428
- >>> NYC = gettz('America/New_York')
429
- >>> NYC
430
- tzfile('/usr/share/zoneinfo/America/New_York')
431
-
432
- >>> print(datetime(2016, 1, 3, tzinfo=NYC)) # EST
433
- 2016-01-03 00:00:00-05:00
434
-
435
- >>> print(datetime(2016, 7, 7, tzinfo=NYC)) # EDT
436
- 2016-07-07 00:00:00-04:00
437
-
438
-
439
- The ``tzfile`` structure contains a fully history of the time zone,
440
- so historical dates will also have the right offsets. For example, before
441
- the adoption of the UTC standards, New York used local solar mean time:
442
-
443
- .. doctest:: tzfile
444
-
445
- >>> print(datetime(1901, 4, 12, tzinfo=NYC)) # LMT
446
- 1901-04-12 00:00:00-04:56
447
-
448
- And during World War II, New York was on "Eastern War Time", which was a
449
- state of permanent daylight saving time:
450
-
451
- .. doctest:: tzfile
452
-
453
- >>> print(datetime(1944, 2, 7, tzinfo=NYC)) # EWT
454
- 1944-02-07 00:00:00-04:00
455
-
456
- """
457
-
458
- def __init__(self, fileobj, filename=None):
459
- super(tzfile, self).__init__()
460
-
461
- file_opened_here = False
462
- if isinstance(fileobj, string_types):
463
- self._filename = fileobj
464
- fileobj = open(fileobj, 'rb')
465
- file_opened_here = True
466
- elif filename is not None:
467
- self._filename = filename
468
- elif hasattr(fileobj, "name"):
469
- self._filename = fileobj.name
470
- else:
471
- self._filename = repr(fileobj)
472
-
473
- if fileobj is not None:
474
- if not file_opened_here:
475
- fileobj = _nullcontext(fileobj)
476
-
477
- with fileobj as file_stream:
478
- tzobj = self._read_tzfile(file_stream)
479
-
480
- self._set_tzdata(tzobj)
481
-
482
- def _set_tzdata(self, tzobj):
483
- """ Set the time zone data of this object from a _tzfile object """
484
- # Copy the relevant attributes over as private attributes
485
- for attr in _tzfile.attrs:
486
- setattr(self, '_' + attr, getattr(tzobj, attr))
487
-
488
- def _read_tzfile(self, fileobj):
489
- out = _tzfile()
490
-
491
- # From tzfile(5):
492
- #
493
- # The time zone information files used by tzset(3)
494
- # begin with the magic characters "TZif" to identify
495
- # them as time zone information files, followed by
496
- # sixteen bytes reserved for future use, followed by
497
- # six four-byte values of type long, written in a
498
- # ``standard'' byte order (the high-order byte
499
- # of the value is written first).
500
- if fileobj.read(4).decode() != "TZif":
501
- raise ValueError("magic not found")
502
-
503
- fileobj.read(16)
504
-
505
- (
506
- # The number of UTC/local indicators stored in the file.
507
- ttisgmtcnt,
508
-
509
- # The number of standard/wall indicators stored in the file.
510
- ttisstdcnt,
511
-
512
- # The number of leap seconds for which data is
513
- # stored in the file.
514
- leapcnt,
515
-
516
- # The number of "transition times" for which data
517
- # is stored in the file.
518
- timecnt,
519
-
520
- # The number of "local time types" for which data
521
- # is stored in the file (must not be zero).
522
- typecnt,
523
-
524
- # The number of characters of "time zone
525
- # abbreviation strings" stored in the file.
526
- charcnt,
527
-
528
- ) = struct.unpack(">6l", fileobj.read(24))
529
-
530
- # The above header is followed by tzh_timecnt four-byte
531
- # values of type long, sorted in ascending order.
532
- # These values are written in ``standard'' byte order.
533
- # Each is used as a transition time (as returned by
534
- # time(2)) at which the rules for computing local time
535
- # change.
536
-
537
- if timecnt:
538
- out.trans_list_utc = list(struct.unpack(">%dl" % timecnt,
539
- fileobj.read(timecnt*4)))
540
- else:
541
- out.trans_list_utc = []
542
-
543
- # Next come tzh_timecnt one-byte values of type unsigned
544
- # char; each one tells which of the different types of
545
- # ``local time'' types described in the file is associated
546
- # with the same-indexed transition time. These values
547
- # serve as indices into an array of ttinfo structures that
548
- # appears next in the file.
549
-
550
- if timecnt:
551
- out.trans_idx = struct.unpack(">%dB" % timecnt,
552
- fileobj.read(timecnt))
553
- else:
554
- out.trans_idx = []
555
-
556
- # Each ttinfo structure is written as a four-byte value
557
- # for tt_gmtoff of type long, in a standard byte
558
- # order, followed by a one-byte value for tt_isdst
559
- # and a one-byte value for tt_abbrind. In each
560
- # structure, tt_gmtoff gives the number of
561
- # seconds to be added to UTC, tt_isdst tells whether
562
- # tm_isdst should be set by localtime(3), and
563
- # tt_abbrind serves as an index into the array of
564
- # time zone abbreviation characters that follow the
565
- # ttinfo structure(s) in the file.
566
-
567
- ttinfo = []
568
-
569
- for i in range(typecnt):
570
- ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
571
-
572
- abbr = fileobj.read(charcnt).decode()
573
-
574
- # Then there are tzh_leapcnt pairs of four-byte
575
- # values, written in standard byte order; the
576
- # first value of each pair gives the time (as
577
- # returned by time(2)) at which a leap second
578
- # occurs; the second gives the total number of
579
- # leap seconds to be applied after the given time.
580
- # The pairs of values are sorted in ascending order
581
- # by time.
582
-
583
- # Not used, for now (but seek for correct file position)
584
- if leapcnt:
585
- fileobj.seek(leapcnt * 8, os.SEEK_CUR)
586
-
587
- # Then there are tzh_ttisstdcnt standard/wall
588
- # indicators, each stored as a one-byte value;
589
- # they tell whether the transition times associated
590
- # with local time types were specified as standard
591
- # time or wall clock time, and are used when
592
- # a time zone file is used in handling POSIX-style
593
- # time zone environment variables.
594
-
595
- if ttisstdcnt:
596
- isstd = struct.unpack(">%db" % ttisstdcnt,
597
- fileobj.read(ttisstdcnt))
598
-
599
- # Finally, there are tzh_ttisgmtcnt UTC/local
600
- # indicators, each stored as a one-byte value;
601
- # they tell whether the transition times associated
602
- # with local time types were specified as UTC or
603
- # local time, and are used when a time zone file
604
- # is used in handling POSIX-style time zone envi-
605
- # ronment variables.
606
-
607
- if ttisgmtcnt:
608
- isgmt = struct.unpack(">%db" % ttisgmtcnt,
609
- fileobj.read(ttisgmtcnt))
610
-
611
- # Build ttinfo list
612
- out.ttinfo_list = []
613
- for i in range(typecnt):
614
- gmtoff, isdst, abbrind = ttinfo[i]
615
- gmtoff = _get_supported_offset(gmtoff)
616
- tti = _ttinfo()
617
- tti.offset = gmtoff
618
- tti.dstoffset = datetime.timedelta(0)
619
- tti.delta = datetime.timedelta(seconds=gmtoff)
620
- tti.isdst = isdst
621
- tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)]
622
- tti.isstd = (ttisstdcnt > i and isstd[i] != 0)
623
- tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0)
624
- out.ttinfo_list.append(tti)
625
-
626
- # Replace ttinfo indexes for ttinfo objects.
627
- out.trans_idx = [out.ttinfo_list[idx] for idx in out.trans_idx]
628
-
629
- # Set standard, dst, and before ttinfos. before will be
630
- # used when a given time is before any transitions,
631
- # and will be set to the first non-dst ttinfo, or to
632
- # the first dst, if all of them are dst.
633
- out.ttinfo_std = None
634
- out.ttinfo_dst = None
635
- out.ttinfo_before = None
636
- if out.ttinfo_list:
637
- if not out.trans_list_utc:
638
- out.ttinfo_std = out.ttinfo_first = out.ttinfo_list[0]
639
- else:
640
- for i in range(timecnt-1, -1, -1):
641
- tti = out.trans_idx[i]
642
- if not out.ttinfo_std and not tti.isdst:
643
- out.ttinfo_std = tti
644
- elif not out.ttinfo_dst and tti.isdst:
645
- out.ttinfo_dst = tti
646
-
647
- if out.ttinfo_std and out.ttinfo_dst:
648
- break
649
- else:
650
- if out.ttinfo_dst and not out.ttinfo_std:
651
- out.ttinfo_std = out.ttinfo_dst
652
-
653
- for tti in out.ttinfo_list:
654
- if not tti.isdst:
655
- out.ttinfo_before = tti
656
- break
657
- else:
658
- out.ttinfo_before = out.ttinfo_list[0]
659
-
660
- # Now fix transition times to become relative to wall time.
661
- #
662
- # I'm not sure about this. In my tests, the tz source file
663
- # is setup to wall time, and in the binary file isstd and
664
- # isgmt are off, so it should be in wall time. OTOH, it's
665
- # always in gmt time. Let me know if you have comments
666
- # about this.
667
- lastdst = None
668
- lastoffset = None
669
- lastdstoffset = None
670
- lastbaseoffset = None
671
- out.trans_list = []
672
-
673
- for i, tti in enumerate(out.trans_idx):
674
- offset = tti.offset
675
- dstoffset = 0
676
-
677
- if lastdst is not None:
678
- if tti.isdst:
679
- if not lastdst:
680
- dstoffset = offset - lastoffset
681
-
682
- if not dstoffset and lastdstoffset:
683
- dstoffset = lastdstoffset
684
-
685
- tti.dstoffset = datetime.timedelta(seconds=dstoffset)
686
- lastdstoffset = dstoffset
687
-
688
- # If a time zone changes its base offset during a DST transition,
689
- # then you need to adjust by the previous base offset to get the
690
- # transition time in local time. Otherwise you use the current
691
- # base offset. Ideally, I would have some mathematical proof of
692
- # why this is true, but I haven't really thought about it enough.
693
- baseoffset = offset - dstoffset
694
- adjustment = baseoffset
695
- if (lastbaseoffset is not None and baseoffset != lastbaseoffset
696
- and tti.isdst != lastdst):
697
- # The base DST has changed
698
- adjustment = lastbaseoffset
699
-
700
- lastdst = tti.isdst
701
- lastoffset = offset
702
- lastbaseoffset = baseoffset
703
-
704
- out.trans_list.append(out.trans_list_utc[i] + adjustment)
705
-
706
- out.trans_idx = tuple(out.trans_idx)
707
- out.trans_list = tuple(out.trans_list)
708
- out.trans_list_utc = tuple(out.trans_list_utc)
709
-
710
- return out
711
-
712
- def _find_last_transition(self, dt, in_utc=False):
713
- # If there's no list, there are no transitions to find
714
- if not self._trans_list:
715
- return None
716
-
717
- timestamp = _datetime_to_timestamp(dt)
718
-
719
- # Find where the timestamp fits in the transition list - if the
720
- # timestamp is a transition time, it's part of the "after" period.
721
- trans_list = self._trans_list_utc if in_utc else self._trans_list
722
- idx = bisect.bisect_right(trans_list, timestamp)
723
-
724
- # We want to know when the previous transition was, so subtract off 1
725
- return idx - 1
726
-
727
- def _get_ttinfo(self, idx):
728
- # For no list or after the last transition, default to _ttinfo_std
729
- if idx is None or (idx + 1) >= len(self._trans_list):
730
- return self._ttinfo_std
731
-
732
- # If there is a list and the time is before it, return _ttinfo_before
733
- if idx < 0:
734
- return self._ttinfo_before
735
-
736
- return self._trans_idx[idx]
737
-
738
- def _find_ttinfo(self, dt):
739
- idx = self._resolve_ambiguous_time(dt)
740
-
741
- return self._get_ttinfo(idx)
742
-
743
- def fromutc(self, dt):
744
- """
745
- The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`.
746
-
747
- :param dt:
748
- A :py:class:`datetime.datetime` object.
749
-
750
- :raises TypeError:
751
- Raised if ``dt`` is not a :py:class:`datetime.datetime` object.
752
-
753
- :raises ValueError:
754
- Raised if this is called with a ``dt`` which does not have this
755
- ``tzinfo`` attached.
756
-
757
- :return:
758
- Returns a :py:class:`datetime.datetime` object representing the
759
- wall time in ``self``'s time zone.
760
- """
761
- # These isinstance checks are in datetime.tzinfo, so we'll preserve
762
- # them, even if we don't care about duck typing.
763
- if not isinstance(dt, datetime.datetime):
764
- raise TypeError("fromutc() requires a datetime argument")
765
-
766
- if dt.tzinfo is not self:
767
- raise ValueError("dt.tzinfo is not self")
768
-
769
- # First treat UTC as wall time and get the transition we're in.
770
- idx = self._find_last_transition(dt, in_utc=True)
771
- tti = self._get_ttinfo(idx)
772
-
773
- dt_out = dt + datetime.timedelta(seconds=tti.offset)
774
-
775
- fold = self.is_ambiguous(dt_out, idx=idx)
776
-
777
- return enfold(dt_out, fold=int(fold))
778
-
779
- def is_ambiguous(self, dt, idx=None):
780
- """
781
- Whether or not the "wall time" of a given datetime is ambiguous in this
782
- zone.
783
-
784
- :param dt:
785
- A :py:class:`datetime.datetime`, naive or time zone aware.
786
-
787
-
788
- :return:
789
- Returns ``True`` if ambiguous, ``False`` otherwise.
790
-
791
- .. versionadded:: 2.6.0
792
- """
793
- if idx is None:
794
- idx = self._find_last_transition(dt)
795
-
796
- # Calculate the difference in offsets from current to previous
797
- timestamp = _datetime_to_timestamp(dt)
798
- tti = self._get_ttinfo(idx)
799
-
800
- if idx is None or idx <= 0:
801
- return False
802
-
803
- od = self._get_ttinfo(idx - 1).offset - tti.offset
804
- tt = self._trans_list[idx] # Transition time
805
-
806
- return timestamp < tt + od
807
-
808
- def _resolve_ambiguous_time(self, dt):
809
- idx = self._find_last_transition(dt)
810
-
811
- # If we have no transitions, return the index
812
- _fold = self._fold(dt)
813
- if idx is None or idx == 0:
814
- return idx
815
-
816
- # If it's ambiguous and we're in a fold, shift to a different index.
817
- idx_offset = int(not _fold and self.is_ambiguous(dt, idx))
818
-
819
- return idx - idx_offset
820
-
821
- def utcoffset(self, dt):
822
- if dt is None:
823
- return None
824
-
825
- if not self._ttinfo_std:
826
- return ZERO
827
-
828
- return self._find_ttinfo(dt).delta
829
-
830
- def dst(self, dt):
831
- if dt is None:
832
- return None
833
-
834
- if not self._ttinfo_dst:
835
- return ZERO
836
-
837
- tti = self._find_ttinfo(dt)
838
-
839
- if not tti.isdst:
840
- return ZERO
841
-
842
- # The documentation says that utcoffset()-dst() must
843
- # be constant for every dt.
844
- return tti.dstoffset
845
-
846
- @tzname_in_python2
847
- def tzname(self, dt):
848
- if not self._ttinfo_std or dt is None:
849
- return None
850
- return self._find_ttinfo(dt).abbr
851
-
852
- def __eq__(self, other):
853
- if not isinstance(other, tzfile):
854
- return NotImplemented
855
- return (self._trans_list == other._trans_list and
856
- self._trans_idx == other._trans_idx and
857
- self._ttinfo_list == other._ttinfo_list)
858
-
859
- __hash__ = None
860
-
861
- def __ne__(self, other):
862
- return not (self == other)
863
-
864
- def __repr__(self):
865
- return "%s(%s)" % (self.__class__.__name__, repr(self._filename))
866
-
867
- def __reduce__(self):
868
- return self.__reduce_ex__(None)
869
-
870
- def __reduce_ex__(self, protocol):
871
- return (self.__class__, (None, self._filename), self.__dict__)
872
-
873
-
874
336
  class tzrange(tzrangebase):
875
337
  """
876
338
  The ``tzrange`` object is a time zone specified by a set of offsets and
@@ -1456,16 +918,7 @@ class tzical(object):
1456
918
  return "%s(%s)" % (self.__class__.__name__, repr(self._s))
1457
919
 
1458
920
 
1459
- if sys.platform != "win32":
1460
- TZFILES = ["/etc/localtime", "localtime"]
1461
- TZPATHS = ["/usr/share/zoneinfo",
1462
- "/usr/lib/zoneinfo",
1463
- "/usr/share/lib/zoneinfo",
1464
- "/etc/zoneinfo"]
1465
- else:
1466
- TZFILES = []
1467
- TZPATHS = []
1468
-
921
+ TZFILES = ["/etc/localtime", "localtime"]
1469
922
 
1470
923
  def __get_gettz():
1471
924
  tzlocal_classes = (tzlocal,)
@@ -1489,7 +942,12 @@ def __get_gettz():
1489
942
  tzfile('/etc/localtime')
1490
943
 
1491
944
  This function is also the preferred way to map IANA tz database keys
1492
- to :class:`tzfile` objects:
945
+ to :class:`tzfile` objects. It searches for these keys in the
946
+ following order:
947
+
948
+ 1. On the search path defined in :data:`TZPATH`.
949
+ 2. In the `tzdata <https://pypi.org/project/tzdata/>`_ Python package.
950
+ 3. Using the system-specific mechanisms (e.g. the Windows registry).
1493
951
 
1494
952
  .. code-block:: python3
1495
953
 
@@ -1554,6 +1012,10 @@ def __get_gettz():
1554
1012
  rv = self.__instances.get(name, None)
1555
1013
 
1556
1014
  if rv is None:
1015
+ # TODO: This holds the cache lock while nocache() reads
1016
+ # the file, so a slow lookup (e.g. walking TZPATH on a
1017
+ # network file system) blocks every other gettz call,
1018
+ # including cache hits for unrelated keys.
1557
1019
  rv = self.nocache(name=name)
1558
1020
  if not (name is None
1559
1021
  or isinstance(rv, tzlocal_classes)
@@ -1600,13 +1062,13 @@ def __get_gettz():
1600
1062
  for filepath in TZFILES:
1601
1063
  if not os.path.isabs(filepath):
1602
1064
  filename = filepath
1603
- for path in TZPATHS:
1065
+ for path in _tzpath.TZPATH:
1604
1066
  filepath = os.path.join(path, filename)
1605
- if os.path.isfile(filepath):
1067
+ if _isfile(filepath):
1606
1068
  break
1607
1069
  else:
1608
1070
  continue
1609
- if os.path.isfile(filepath):
1071
+ if _isfile(filepath):
1610
1072
  try:
1611
1073
  tz = tzfile(filepath)
1612
1074
  break
@@ -1624,25 +1086,59 @@ def __get_gettz():
1624
1086
  six.raise_from(TypeError(new_msg), e)
1625
1087
  else:
1626
1088
  raise
1089
+
1090
+ # A NUL byte can never be part of a key or a path, and
1091
+ # depending on the platform and Python version it raises
1092
+ # ValueError from os.path, winreg or open(), so reject it
1093
+ # up front rather than catching it in every branch.
1094
+ if "\x00" in name:
1095
+ return None
1096
+
1627
1097
  if os.path.isabs(name):
1628
- if os.path.isfile(name):
1098
+ if _isfile(name):
1629
1099
  tz = tzfile(name)
1630
1100
  else:
1631
1101
  tz = None
1632
1102
  else:
1633
- for path in TZPATHS:
1103
+ if _escapes_tzpath(name):
1104
+ _warn_tzpath_escape(name)
1105
+
1106
+ for path in _tzpath.TZPATH:
1634
1107
  filepath = os.path.join(path, name)
1635
- if not os.path.isfile(filepath):
1108
+ if not _isfile(filepath):
1636
1109
  filepath = filepath.replace(' ', '_')
1637
- if not os.path.isfile(filepath):
1110
+ if not _isfile(filepath):
1638
1111
  continue
1639
1112
  try:
1640
- tz = tzfile(filepath)
1113
+ tz = tzfile(filepath, key=name)
1641
1114
  break
1642
1115
  except (IOError, OSError, ValueError):
1116
+ # TODO: A file that exists but can't be read or
1117
+ # isn't a valid TZif file is silently skipped,
1118
+ # and the key is looked up in the next TZPATH
1119
+ # entry and then in tzdata. Consider warning
1120
+ # here, since this usually means the system time
1121
+ # zone data is broken.
1643
1122
  pass
1644
1123
  else:
1645
1124
  tz = None
1125
+ # As with the search path lookup above, also try
1126
+ # the name with spaces replaced by underscores.
1127
+ candidates = [name]
1128
+ if " " in name:
1129
+ candidates.append(name.replace(" ", "_"))
1130
+
1131
+ for candidate in candidates:
1132
+ try:
1133
+ with _tzdata_impl._load_tzdata(candidate) as f:
1134
+ return tzfile(f, key=name)
1135
+ except ValueError:
1136
+ # TZFileNotFound is a ValueError, and so is
1137
+ # the error raised for files in tzdata that
1138
+ # are not TZif files (e.g. "zone.tab"),
1139
+ # which are skipped just as on TZPATH.
1140
+ pass
1141
+
1646
1142
  if tzwin is not None:
1647
1143
  try:
1648
1144
  tz = tzwin(name)
@@ -1650,10 +1146,6 @@ def __get_gettz():
1650
1146
  # UnicodeEncodeError is for Python 2.7 compat
1651
1147
  tz = None
1652
1148
 
1653
- if not tz:
1654
- from dateutil.zoneinfo import get_zonefile_instance
1655
- tz = get_zonefile_instance().get(name)
1656
-
1657
1149
  if not tz:
1658
1150
  for c in name:
1659
1151
  # name is not a tzstr unless it has at least
@@ -1680,6 +1172,123 @@ gettz = __get_gettz()
1680
1172
  del __get_gettz
1681
1173
 
1682
1174
 
1175
+ class DeprecatedTzKeyWarning(FutureWarning):
1176
+ """Warning raised when :func:`gettz` is given a key outside of TZPATH.
1177
+
1178
+ Relative keys like ``"../../etc/localtime"`` resolve to files outside of
1179
+ every directory on :data:`TZPATH`. Support for these is deprecated, and a
1180
+ warnings filter turns this warning into an error by default. To keep the
1181
+ old behavior for now, filter it out, e.g.::
1182
+
1183
+ warnings.filterwarnings(
1184
+ "ignore", category=dateutil.tz.DeprecatedTzKeyWarning
1185
+ )
1186
+
1187
+ Absolute paths to TZif files are not affected.
1188
+ """
1189
+
1190
+
1191
+ def _escapes_tzpath(name):
1192
+ # A relative key stays inside every TZPATH entry unless, once normalized
1193
+ # (which also turns os.altsep into os.sep), it starts by going up a
1194
+ # directory.
1195
+ return os.path.normpath(name).split(os.sep, 1)[0] == os.pardir
1196
+
1197
+
1198
+ def _warn_tzpath_escape(name):
1199
+ # As in dateutil.zoneinfo.rebuild, the filter is appended at call time,
1200
+ # so any filter the user has installed for this category wins.
1201
+ warnings.filterwarnings(
1202
+ "error", category=DeprecatedTzKeyWarning, append=True
1203
+ )
1204
+ warnings.warn(
1205
+ "gettz(%r) refers to a file outside of TZPATH; support for keys "
1206
+ "like this is deprecated and will be removed in a future version. "
1207
+ "Use an absolute path instead." % (name,),
1208
+ DeprecatedTzKeyWarning,
1209
+ stacklevel=4,
1210
+ )
1211
+
1212
+
1213
+ def available_iana_timezones():
1214
+ """Returns a set containing all available IANA time zones keys.
1215
+
1216
+ These are suitable for use with :func:`gettz`. This is essentially a direct
1217
+ port of :py:func:`zoneinfo.available_timezones`.
1218
+
1219
+ The search includes time zones found on :data:`TZPATH` as well as any
1220
+ time zones provided by the `tzdata <https://pypi.org/project/tzdata/>`_
1221
+ package if it is installed.
1222
+
1223
+ :return:
1224
+ Returns a mutable set containing strings of time zones. Each call will
1225
+ return a fresh set.
1226
+
1227
+ .. caution::
1228
+
1229
+ This may attempt to open a large number of files, since the best way to
1230
+ determine if a given file on the time zone search path is to open it
1231
+ and check for the "magic string" at the beginning.
1232
+
1233
+ .. note::
1234
+
1235
+ These values are not designed to be exposed to end-users; for
1236
+ user-facing elements, applications should use something like CLDR (the
1237
+ Unicode Common Locale Data Repository) to get more user-friendly
1238
+ strings.
1239
+ """
1240
+ # Start with loading from the tzdata package if it exists: this has a
1241
+ # pre-assembled list of zones that only requires opening one file.
1242
+ try:
1243
+ valid_zones = set(_tzdata_impl._load_tzdata_keys())
1244
+ except ImportError:
1245
+ valid_zones = set()
1246
+
1247
+ def valid_key(fpath):
1248
+ try:
1249
+ with open(fpath, "rb") as f:
1250
+ return f.read(4) == b"TZif"
1251
+ except Exception: # pragma: nocover
1252
+ return False
1253
+
1254
+ for tz_root in _tzpath.TZPATH:
1255
+ if not os.path.exists(tz_root):
1256
+ continue
1257
+
1258
+ for root, dirnames, files in os.walk(tz_root):
1259
+ if root == tz_root:
1260
+ # right/ and posix/ are special directories and shouldn't be
1261
+ # included in the output of available zones
1262
+ if "right" in dirnames:
1263
+ dirnames.remove("right")
1264
+ if "posix" in dirnames:
1265
+ dirnames.remove("posix")
1266
+
1267
+ for file in files:
1268
+ fpath = os.path.join(root, file)
1269
+
1270
+ key = os.path.relpath(fpath, start=tz_root)
1271
+ if os.sep != "/": # pragma: nocover
1272
+ key = key.replace(os.sep, "/")
1273
+
1274
+ if not key or key in valid_zones:
1275
+ continue
1276
+
1277
+ if valid_key(fpath):
1278
+ valid_zones.add(key)
1279
+
1280
+ if "posixrules" in valid_zones:
1281
+ # posixrules is a special symlink-only time zone where it exists, it
1282
+ # should not be included in the output
1283
+ valid_zones.remove("posixrules")
1284
+ if "localtime" in valid_zones:
1285
+ # localtime is a special symlink-only time zone where it exists, it
1286
+ # should not be included in the output
1287
+ valid_zones.remove("localtime")
1288
+
1289
+ return valid_zones
1290
+
1291
+
1683
1292
  def datetime_exists(dt, tz=None):
1684
1293
  """
1685
1294
  Given a datetime and a time zone, determine whether or not a given datetime
@@ -1828,6 +1437,20 @@ else:
1828
1437
  return calculated_offset
1829
1438
 
1830
1439
 
1440
+ if sys.version_info < (3, 8):
1441
+
1442
+ def _isfile(path):
1443
+ # bpo-33721: In Python 3.8 non-UTF8 paths return False rather than
1444
+ # raising an error. See https://bugs.python.org/issue33721
1445
+ try:
1446
+ return os.path.isfile(path)
1447
+ except ValueError:
1448
+ return False
1449
+
1450
+ else:
1451
+ _isfile = os.path.isfile
1452
+
1453
+
1831
1454
  try:
1832
1455
  # Python 3.7 feature
1833
1456
  from contextlib import nullcontext as _nullcontext