Skip to content

Commit 39842c1

Browse files
committed
Fix usage of zoneinfo timezones
1 parent 9074698 commit 39842c1

File tree

18 files changed

+628
-716
lines changed

18 files changed

+628
-716
lines changed

clock

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ from babel.plural import _binary_compiler
1616
from babel.plural import _GettextCompiler
1717
from babel.plural import _unary_compiler
1818
from babel.plural import compile_zero
19-
from cleo import Application
20-
from cleo import Command
21-
from cleo import argument
19+
from cleo.application import Application
20+
from cleo.commands.command import Command
21+
from cleo.helpers import argument
2222

2323
from pendulum import __version__
2424

@@ -47,7 +47,7 @@ class _LambdaCompiler(_GettextCompiler):
4747

4848
class LocaleCreate(Command):
4949

50-
name = "create"
50+
name = "locale create"
5151
description = "Creates locale translations."
5252

5353
arguments = [argument("locales", "Locales to dump.", optional=False, multiple=True)]
@@ -236,7 +236,7 @@ translations = {{}}
236236

237237
class LocaleRecreate(Command):
238238

239-
name = "recreate"
239+
name = "locale recreate"
240240
description = "Recreate existing locales."
241241

242242
def handle(self):
@@ -249,20 +249,9 @@ class LocaleRecreate(Command):
249249
self.call("locale:create", [("locales", locales)])
250250

251251

252-
class LocaleCommand(Command):
253-
254-
name = "locale"
255-
description = "Locale related commands."
256-
257-
commands = [LocaleCreate()]
258-
259-
def handle(self):
260-
self.call("help", self._config.name)
261-
262-
263252
class WindowsTzDump(Command):
264253

265-
name = "dump-timezones"
254+
name = "windows dump-timezones"
266255
description = "Dumps the mapping of Windows timezones to IANA timezones."
267256

268257
MAPPING_DIR = os.path.join("pendulum", "tz", "data")
@@ -281,21 +270,14 @@ class WindowsTzDump(Command):
281270
with open(os.path.join(self.MAPPING_DIR, "windows.py"), "w") as f:
282271
f.write(mapping)
283272

284-
285-
class WindowsCommand(Command):
286-
287-
name = "windows"
288-
description = "Windows related commands."
289-
290-
commands = [WindowsTzDump()]
291-
292273
def handle(self):
293274
self.call("help", self._config.name)
294275

295276

296277
app = Application("clock", __version__)
297-
app.add(LocaleCommand())
298-
app.add(WindowsCommand())
278+
app.add(LocaleCreate())
279+
app.add(LocaleRecreate())
280+
app.add(WindowsTzDump())
299281

300282

301283
if __name__ == "__main__":

pendulum/__init__.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,21 @@ def datetime(
101101
second: int = 0,
102102
microsecond: int = 0,
103103
tz: Optional[Union[str, float, Timezone]] = UTC,
104-
dst_rule: str = POST_TRANSITION,
104+
fold: Optional[int] = 1,
105+
raise_on_unknown_times: bool = False,
105106
) -> DateTime:
106107
"""
107108
Creates a new DateTime instance from a specific date and time.
108109
"""
109110
if tz is not None:
110111
tz = _safe_timezone(tz)
111112

112-
dt = _datetime.datetime(year, month, day, hour, minute, second, microsecond)
113+
dt = _datetime.datetime(
114+
year, month, day, hour, minute, second, microsecond, fold=fold
115+
)
113116

114117
if tz is not None:
115-
dt = tz.convert(dt, dst_rule=dst_rule)
118+
dt = tz.convert(dt, raise_on_unknown_times=raise_on_unknown_times)
116119

117120
return DateTime(
118121
dt.year,
@@ -152,11 +155,12 @@ def naive(
152155
minute: int = 0,
153156
second: int = 0,
154157
microsecond: int = 0,
158+
fold: Optional[int] = 1,
155159
) -> DateTime:
156160
"""
157161
Return a naive DateTime.
158162
"""
159-
return DateTime(year, month, day, hour, minute, second, microsecond)
163+
return DateTime(year, month, day, hour, minute, second, microsecond, fold=fold)
160164

161165

162166
def date(year: int, month: int, day: int) -> Date:
@@ -223,7 +227,7 @@ def now(tz: Optional[Union[str, Timezone]] = None) -> DateTime:
223227
else:
224228
dt = _datetime.datetime.now(UTC)
225229
tz = _safe_timezone(tz)
226-
dt = tz.convert(dt)
230+
dt = dt.astimezone(tz)
227231

228232
return DateTime(
229233
dt.year,

pendulum/_extensions/helpers.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,6 @@ def precise_diff(
197197
198198
:rtype: PreciseDiff
199199
"""
200-
print("DT", d1, d2)
201200
sign = 1
202201

203202
if d1 == d2:

pendulum/datetime.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,11 @@ def tz(self) -> Optional[Timezone]:
175175
return self.timezone
176176

177177
@property
178-
def timezone_name(self) -> str:
178+
def timezone_name(self) -> Optional[str]:
179179
tz = self.timezone
180180

181181
if tz is None:
182-
return None
182+
return
183183

184184
return tz.name
185185

@@ -191,7 +191,7 @@ def is_local(self) -> bool:
191191
return self.offset == self.in_timezone(pendulum.local_timezone()).offset
192192

193193
def is_utc(self) -> bool:
194-
return self.offset == UTC.offset
194+
return self.offset == 0
195195

196196
def is_dst(self) -> bool:
197197
return self.dst() != datetime.timedelta()
@@ -241,7 +241,11 @@ def in_timezone(self, tz: Union[str, Timezone]) -> "DateTime":
241241
"""
242242
tz = pendulum._safe_timezone(tz)
243243

244-
return tz.convert(self, dst_rule=pendulum.POST_TRANSITION)
244+
dt = self
245+
if not self.timezone:
246+
dt = dt.replace(fold=1)
247+
248+
return tz.convert(dt)
245249

246250
def in_tz(self, tz: Union[str, Timezone]) -> "DateTime":
247251
"""
@@ -377,7 +381,7 @@ def __repr__(self) -> str:
377381
minute=self.minute,
378382
second=self.second,
379383
us=us,
380-
tzinfo=self.tzinfo,
384+
tzinfo=repr(self.tzinfo),
381385
)
382386

383387
# Comparisons
@@ -469,8 +473,7 @@ def add(
469473
Add a duration to the instance.
470474
471475
If we're adding units of variable length (i.e., years, months),
472-
move forward from curren time,
473-
otherwise move forward from utc, for accuracy
476+
move forward from current time, otherwise move forward from utc, for accuracy
474477
when moving across DST boundaries.
475478
"""
476479
units_of_variable_length = any([years, months, weeks, days])
@@ -1170,7 +1173,19 @@ def combine(cls, date: datetime.date, time: datetime.time) -> "DateTime":
11701173
return pendulum.instance(datetime.datetime.combine(date, time), tz=None)
11711174

11721175
def astimezone(self, tz: Optional[datetime.tzinfo] = None) -> "DateTime":
1173-
return pendulum.instance(super(DateTime, self).astimezone(tz))
1176+
dt = super().astimezone(tz)
1177+
1178+
return self.__class__(
1179+
dt.year,
1180+
dt.month,
1181+
dt.day,
1182+
dt.hour,
1183+
dt.minute,
1184+
dt.second,
1185+
dt.microsecond,
1186+
fold=dt.fold,
1187+
tzinfo=dt.tzinfo,
1188+
)
11741189

11751190
def replace(
11761191
self,
@@ -1203,22 +1218,8 @@ def replace(
12031218
if fold is None:
12041219
fold = self.fold
12051220

1206-
transition_rule = pendulum.POST_TRANSITION
1207-
if fold is not None:
1208-
transition_rule = pendulum.PRE_TRANSITION
1209-
if fold:
1210-
transition_rule = pendulum.POST_TRANSITION
1211-
12121221
return pendulum.datetime(
1213-
year,
1214-
month,
1215-
day,
1216-
hour,
1217-
minute,
1218-
second,
1219-
microsecond,
1220-
tz=tzinfo,
1221-
dst_rule=transition_rule,
1222+
year, month, day, hour, minute, second, microsecond, tz=tzinfo, fold=fold
12221223
)
12231224

12241225
def __getnewargs__(self) -> Tuple:

pendulum/formatting/formatter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ def _get_parsed_value(
601601
parsed["tz"] = pendulum.timezone(offset)
602602
elif token == "z":
603603
# Full timezone
604-
if value not in pendulum.timezones:
604+
if value not in pendulum.timezones():
605605
raise ValueError("Invalid date")
606606

607607
parsed["tz"] = pendulum.timezone(value)

pendulum/tz/local_timezone.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from pendulum.utils._compat import zoneinfo
1111

12+
from .exceptions import InvalidTimezone
1213
from .timezone import FixedTimezone
1314
from .timezone import Timezone
1415

@@ -213,7 +214,7 @@ def _get_unix_timezone(_root="/"): # type: (str) -> Timezone
213214

214215
try:
215216
return Timezone(os.path.join(*tzpath))
216-
except zoneinfo.ZoneInfoNotFoundError:
217+
except InvalidTimezone:
217218
pass
218219

219220
# systemd distributions use symlinks that include the zone name,
@@ -228,7 +229,7 @@ def _get_unix_timezone(_root="/"): # type: (str) -> Timezone
228229
tzpath.insert(0, parts.pop(0))
229230
try:
230231
return Timezone(os.path.join(*tzpath))
231-
except zoneinfo.ZoneInfoNotFoundError:
232+
except InvalidTimezone:
232233
pass
233234

234235
# No explicit setting existed. Use localtime

pendulum/tz/timezone.py

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88

99
from pendulum.utils._compat import zoneinfo
1010

11+
from .exceptions import AmbiguousTime
12+
from .exceptions import InvalidTimezone
13+
from .exceptions import NonExistingTime
14+
1115

1216
POST_TRANSITION = "post"
1317
PRE_TRANSITION = "pre"
@@ -51,11 +55,17 @@ class Timezone(zoneinfo.ZoneInfo, PendulumTimezone):
5155
>>> tz = Timezone('Europe/Paris')
5256
"""
5357

58+
def __new__(cls, key: str) -> "Timezone":
59+
try:
60+
return super().__new__(cls, key)
61+
except zoneinfo.ZoneInfoNotFoundError:
62+
raise InvalidTimezone(key)
63+
5464
@property
5565
def name(self) -> str:
5666
return self.key
5767

58-
def convert(self, dt: datetime, dst_rule: Optional[str] = None) -> datetime:
68+
def convert(self, dt: datetime, raise_on_unknown_times: bool = False) -> datetime:
5969
"""
6070
Converts a datetime in the current timezone.
6171
@@ -76,14 +86,30 @@ def convert(self, dt: datetime, dst_rule: Optional[str] = None) -> datetime:
7686
>>> in_new_york.isoformat()
7787
'2013-03-30T21:30:00-04:00'
7888
"""
79-
if dst_rule is not None:
80-
if dst_rule == PRE_TRANSITION and dt.fold != 0:
81-
dt = dt.replace(fold=0)
82-
elif dst_rule == POST_TRANSITION and dt.fold != 1:
83-
dt = dt.replace(fold=1)
84-
8589
if dt.tzinfo is None:
86-
dt = dt.replace(tzinfo=self)
90+
offset_before = (
91+
self.utcoffset(dt.replace(fold=0)) if dt.fold else self.utcoffset(dt)
92+
)
93+
offset_after = (
94+
self.utcoffset(dt) if dt.fold else self.utcoffset(dt.replace(fold=1))
95+
)
96+
97+
if offset_after > offset_before:
98+
# Skipped time
99+
if raise_on_unknown_times:
100+
raise NonExistingTime(dt)
101+
102+
dt += (
103+
(offset_after - offset_before)
104+
if dt.fold
105+
else (offset_before - offset_after)
106+
)
107+
elif offset_before > offset_after:
108+
# Repeated time
109+
if raise_on_unknown_times:
110+
raise AmbiguousTime(dt)
111+
112+
return dt.replace(tzinfo=self)
87113

88114
return dt.astimezone(self)
89115

@@ -100,10 +126,13 @@ def datetime(
100126
"""
101127
Return a normalized datetime for the current timezone.
102128
"""
103-
return datetime(
104-
year, month, day, hour, minute, second, microsecond, tzinfo=self, fold=1
129+
return self.convert(
130+
datetime(year, month, day, hour, minute, second, microsecond, fold=1)
105131
)
106132

133+
def __repr__(self) -> str:
134+
return f"{self.__class__.__name__}('{self.name}')"
135+
107136

108137
class FixedTimezone(tzinfo, PendulumTimezone):
109138
def __init__(self, offset: int, name: Optional[str] = None) -> None:
@@ -123,7 +152,7 @@ def __init__(self, offset: int, name: Optional[str] = None) -> None:
123152
def name(self) -> str:
124153
return self._name
125154

126-
def convert(self, dt: datetime, dst_rule: Optional[str] = None) -> datetime:
155+
def convert(self, dt: datetime, raise_on_unknown_times: bool = False) -> datetime:
127156
if dt.tzinfo is None:
128157
return dt.__class__(
129158
dt.year,
@@ -137,12 +166,6 @@ def convert(self, dt: datetime, dst_rule: Optional[str] = None) -> datetime:
137166
fold=0,
138167
)
139168

140-
if dst_rule is not None:
141-
if dst_rule == PRE_TRANSITION and dt.fold != 0:
142-
dt = dt.replace(fold=0)
143-
elif dst_rule == POST_TRANSITION and dt.fold != 1:
144-
dt = dt.replace(fold=1)
145-
146169
return dt.astimezone(self)
147170

148171
def datetime(
@@ -179,5 +202,12 @@ def tzname(self, dt: Optional[datetime]) -> Optional[str]:
179202
def __getinitargs__(self): # type: () -> tuple
180203
return self._offset, self._name
181204

205+
def __repr__(self) -> str:
206+
name = ""
207+
if self._name:
208+
name = f', name="{self._name}"'
209+
210+
return f"{self.__class__.__name__}({self._offset}{name})"
211+
182212

183-
UTC = FixedTimezone(0, "UTC")
213+
UTC = Timezone("UTC")

0 commit comments

Comments
 (0)