Skip to content

Commit 850ff4c

Browse files
mayfieldsdispater
authored andcommitted
Refactor Parser into Loader (#35)
* `Parser.parse` -> `Loader.load` * `Parser._parse -> `Loader._load` * Use `pytz.open_resource` for zone file loading to fix pytz installs using shared zoneinfo files. * Fix for python2 setup and test (backport `FileNotFoundError` name). Tested on Fedora Core 24 (linux), python2.7 (distro), python3.5 (distro) Fixes #34
1 parent 2c6d39b commit 850ff4c

File tree

6 files changed

+137
-138
lines changed

6 files changed

+137
-138
lines changed

pendulum/_compat.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
long = long
1313
unicode = unicode
1414
basestring = basestring
15+
FileNotFoundError = IOError
1516

1617
else:
1718

19+
FileNotFoundError = FileNotFoundError
1820
long = int
1921
unicode = str
2022
basestring = str

pendulum/tz/loader.py

Lines changed: 116 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
11
# -*- coding: utf-8 -*-
22

3+
import inspect
34
import os
45
import pytz
5-
import inspect
66

7-
from .parser import Parser
8-
from .._compat import decode
7+
from datetime import datetime
8+
from struct import unpack, calcsize
9+
10+
from .. import _compat
11+
from .breakdown import local_time
12+
from .transition import Transition
13+
from .transition_type import TransitionType
14+
15+
16+
def _byte_string(s):
17+
"""Cast a string or byte string to an ASCII byte string."""
18+
return s.encode('US-ASCII')
19+
20+
_NULL = _byte_string('\0')
21+
22+
23+
def _std_string(s):
24+
"""Cast a string or byte string to an ASCII string."""
25+
return str(s.decode('US-ASCII'))
926

1027

1128
class Loader(object):
@@ -14,17 +31,104 @@ class Loader(object):
1431

1532
@classmethod
1633
def load(cls, name):
17-
name = decode(name)
34+
name = _compat.decode(name)
35+
try:
36+
with pytz.open_resource(name) as f:
37+
return cls._load(f)
38+
except _compat.FileNotFoundError:
39+
raise ValueError('Unknown timezone [{}]'.format(name))
1840

19-
name_parts = name.lstrip('/').split('/')
41+
@classmethod
42+
def _load(cls, fp):
43+
head_fmt = '>4s c 15x 6l'
44+
head_size = calcsize(head_fmt)
45+
(magic, fmt, ttisgmtcnt, ttisstdcnt, leapcnt, timecnt,
46+
typecnt, charcnt) = unpack(head_fmt, fp.read(head_size))
2047

21-
for part in name_parts:
22-
if part == os.path.pardir or os.path.sep in part:
23-
raise ValueError('Bad path segment: %r' % part)
48+
# Make sure it is a tzfile(5) file
49+
assert magic == _byte_string('TZif'), 'Got magic %s' % repr(magic)
2450

25-
filepath = os.path.join(cls.path, *name_parts)
51+
# Read out the transition times,
52+
# localtime indices and ttinfo structures.
53+
data_fmt = '>%(timecnt)dl %(timecnt)dB %(ttinfo)s %(charcnt)ds' % dict(
54+
timecnt=timecnt, ttinfo='lBB' * typecnt, charcnt=charcnt)
55+
data_size = calcsize(data_fmt)
56+
data = unpack(data_fmt, fp.read(data_size))
2657

27-
if not os.path.exists(filepath):
28-
raise ValueError('Unknown timezone [{}]'.format(name))
58+
# make sure we unpacked the right number of values
59+
assert len(data) == 2 * timecnt + 3 * typecnt + 1
60+
transition_times = tuple(trans for trans in data[:timecnt])
61+
lindexes = tuple(data[timecnt:2 * timecnt])
62+
ttinfo_raw = data[2 * timecnt:-1]
63+
tznames_raw = data[-1]
64+
del data
65+
66+
# Process ttinfo into separate structs
67+
transition_types = tuple()
68+
tznames = {}
69+
i = 0
70+
while i < len(ttinfo_raw):
71+
# have we looked up this timezone name yet?
72+
tzname_offset = ttinfo_raw[i + 2]
73+
if tzname_offset not in tznames:
74+
nul = tznames_raw.find(_NULL, tzname_offset)
75+
if nul < 0:
76+
nul = len(tznames_raw)
77+
tznames[tzname_offset] = _std_string(
78+
tznames_raw[tzname_offset:nul])
79+
transition_types += (
80+
TransitionType(
81+
ttinfo_raw[i], bool(ttinfo_raw[i + 1]),
82+
tznames[tzname_offset]
83+
),
84+
)
85+
i += 3
86+
87+
# Now build the timezone object
88+
if len(transition_times) == 0:
89+
transitions = tuple()
90+
else:
91+
# calculate transition info
92+
transitions = tuple()
93+
for i in range(len(transition_times)):
94+
transition_type = transition_types[lindexes[i]]
95+
96+
if i == 0:
97+
pre_transition_type = transition_types[lindexes[i]]
98+
else:
99+
pre_transition_type = transition_types[lindexes[i - 1]]
100+
101+
pre_time = datetime(*local_time(transition_times[i],
102+
pre_transition_type)[:7])
103+
time = datetime(*local_time(transition_times[i],
104+
transition_type)[:7])
105+
tr = Transition(
106+
transition_times[i],
107+
transition_type,
108+
pre_time,
109+
time,
110+
pre_transition_type
111+
)
112+
113+
transitions += (tr,)
114+
115+
# Determine the before-first-transition type
116+
default_transition_type_index = 0
117+
if transitions:
118+
index = 0
119+
if transition_types[0].is_dst:
120+
index = transition_types.index(transitions[0].transition_type)
121+
while index != 0 and transition_types[index].is_dst:
122+
index -= 1
123+
124+
while index != len(transitions) and transition_types[index].is_dst:
125+
index += 1
126+
127+
if index != len(transitions):
128+
default_transition_type_index = index
29129

30-
return Parser.parse(filepath)
130+
return (
131+
transitions,
132+
transition_types,
133+
transition_types[default_transition_type_index]
134+
)

pendulum/tz/local_timezone.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from contextlib import contextmanager
88

99
from .timezone import Timezone
10-
from .parser import Parser
10+
from .loader import Loader
1111

1212

1313
class LocalTimezone(object):
@@ -165,7 +165,7 @@ def get_tz_name_for_unix(cls):
165165

166166
if not os.path.exists(tzpath):
167167
continue
168-
return Timezone('', *Parser.parse(tzpath))
168+
return Timezone('', *Loader.load(tzpath))
169169

170170
raise RuntimeError('Can not find any timezone configuration')
171171

@@ -176,7 +176,7 @@ def _tz_from_env(tzenv):
176176

177177
# TZ specifies a file
178178
if os.path.exists(tzenv):
179-
return Timezone('', *Parser.parse(tzenv))
179+
return Timezone('', *Loader.load(tzenv))
180180

181181
# TZ specifies a zoneinfo zone.
182182
try:

pendulum/tz/parser.py

Lines changed: 0 additions & 123 deletions
This file was deleted.

setup.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
from distutils.command.build_ext import build_ext
99

1010

11+
try:
12+
FileNotFoundError
13+
except NameError:
14+
FileNotFoundError = IOError # py2k
15+
16+
1117
def get_version():
1218
basedir = os.path.dirname(__file__)
1319
with open(os.path.join(basedir, 'pendulum/version.py')) as f:

tests/tz_tests/test_timezone.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pendulum
44
from datetime import datetime
55
from pendulum import timezone
6+
from pendulum.tz.loader import Loader
67

78
from .. import AbstractTestCase
89

@@ -235,3 +236,12 @@ def test_convert_accept_pendulum_instance(self):
235236

236237
self.assertIsInstanceOfPendulum(new)
237238
self.assertPendulum(new, 2016, 8, 7, 14, 53, 54)
239+
240+
241+
class TimezoneLoaderTest(AbstractTestCase):
242+
243+
def test_load_bad_timezone(self):
244+
self.assertRaises(ValueError, Loader.load, '---NOT A TIMEZONE---')
245+
246+
def test_load_valid(self):
247+
self.assertTrue(Loader.load('America/Toronto'))

0 commit comments

Comments
 (0)