Skip to content

Commit 195efbc

Browse files
Use zoneinfo as single source of truth and tz as interface point
1 parent c84801f commit 195efbc

File tree

3 files changed

+39
-30
lines changed

3 files changed

+39
-30
lines changed

pvlib/location.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import pathlib
88
import datetime
9+
import zoneinfo
910

1011
import pandas as pd
1112
import pytz
@@ -21,12 +22,15 @@ class Location:
2122
time zone, and altitude data associated with a particular geographic
2223
location. You can also assign a name to a location object.
2324
24-
Location objects have two time-zone attributes, either of which can be
25-
individually changed after the Location object has been instantiated and
26-
the other will stay in sync:
25+
Location objects have two time-zone attributes:
2726
28-
* ``tz`` is a IANA time-zone string.
29-
* ``pytz`` is a pytz time-zone object.
27+
* ``tz`` is an IANA time-zone string.
28+
* ``pytz`` is a pytz-based time-zone object (read-only).
29+
* ``zoneinfo`` is a zoneinfo.ZoneInfo time-zone object (read-only).
30+
31+
As with Location-object initialization, use the ``tz`` attribute update
32+
the Location's time zone after instantiation, and the read-only ``pytz``
33+
and ``zoneinfo`` attributes will stay in sync with any change in ``tz``.
3034
3135
Location objects support the print method.
3236
@@ -42,14 +46,16 @@ class Location:
4246
4347
tz : time zone as str, int, float, or datetime.tzinfo (inc. subclasses
4448
from the pytz and zoneinfo packages), default 'UTC'.
45-
See http://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a
46-
list of valid name strings for IANA time zones.
47-
ints and floats must be non-fractional N-hour offsets from UTC, which
48-
are converted to the 'Etc/GMT-N' format (note limited range of N and
49-
its conventional sign change).
49+
This value is stored as a valid IANA time zone name string. See
50+
http://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a
51+
list of valid name strings, any of which may be passed directly here.
52+
ints and floats must be whole-number hour offsets from UTC, which
53+
are converted to the IANA-suppored 'Etc/GMT-N' format (note the
54+
limited range of the offset N and its sign-change convention).
5055
Raises TypeError for time zone conversion issues or
51-
pytz.exceptions.UnknownTimeZoneError when (stringified) time zone is
52-
not recognized by pytz.timezone.
56+
zoneinfo.ZoneInfoNotFoundError when the (stringified) time zone is
57+
not recognized as an IANA time zone by the zoneinfo.ZoneInfo
58+
initializer used for internal time-zone representation.
5359
5460
altitude : float, optional
5561
Altitude from sea level in meters.
@@ -87,33 +93,41 @@ def __repr__(self):
8793

8894
@property
8995
def tz(self):
90-
# self.pytz holds the single source of time-zone truth.
91-
return self.pytz.zone
96+
return str(self._zoneinfo)
9297

9398
@tz.setter
9499
def tz(self, tz_):
100+
# self._zoneinfo holds single source of time-zone truth as IANA name.
95101
if isinstance(tz_, str):
96-
self.pytz = pytz.timezone(tz_)
102+
self._zoneinfo = zoneinfo.ZoneInfo(tz_)
97103
elif isinstance(tz_, int):
98-
self.pytz = pytz.timezone(f"Etc/GMT{-tz_:+d}")
104+
self._zoneinfo = zoneinfo.ZoneInfo(f"Etc/GMT{-tz_:+d}")
99105
elif isinstance(tz_, float):
100106
if tz_ % 1 != 0:
101107
raise TypeError(
102-
"floating point tz does not have zero fractional part: "
103-
f"{tz_}"
108+
"Floating-point tz has non-zero fractional part: "
109+
f"{tz_}. Only whole-number offsets are supported."
104110
)
105111

106-
self.pytz = pytz.timezone(f"Etc/GMT{-int(tz_):+d}")
112+
self._zoneinfo = zoneinfo.ZoneInfo(f"Etc/GMT{-int(tz_):+d}")
107113
elif isinstance(tz_, datetime.tzinfo):
108114
# Includes time zones generated by pytz and zoneinfo packages.
109-
self.pytz = pytz.timezone(str(tz_))
115+
self._zoneinfo = zoneinfo.ZoneInfo(str(tz_))
110116
else:
111117
raise TypeError(
112118
f"invalid tz specification: {tz_}, must be an IANA time zone "
113-
"string, a non-fractional int/float UTC offset, or a "
119+
"string, a whole-number int/float UTC offset, or a "
114120
"datetime.tzinfo object (including subclasses)"
115121
)
116122

123+
@property
124+
def pytz(self):
125+
return pytz.timezone(str(self._zoneinfo))
126+
127+
@property
128+
def zoneinfo(self):
129+
return self._zoneinfo
130+
117131
@classmethod
118132
def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs):
119133
"""

pvlib/tests/test_location.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import pytest
1111

1212
import pytz
13-
from pytz.exceptions import UnknownTimeZoneError
1413

1514
import pvlib
1615
from pvlib import location
@@ -62,11 +61,6 @@ def test_location_tz_update():
6261
assert loc.tz == 'Etc/GMT-7'
6362
assert loc.pytz == pytz.timezone('Etc/GMT-7')
6463

65-
# Updating pytz updates tz.
66-
loc.pytz = pytz.timezone('US/Arizona')
67-
assert loc.tz == 'US/Arizona'
68-
assert loc.pytz == pytz.timezone('US/Arizona')
69-
7064

7165
@pytest.mark.parametrize(
7266
'tz', [
@@ -76,7 +70,7 @@ def test_location_tz_update():
7670
]
7771
)
7872
def test_location_invalid_tz(tz):
79-
with pytest.raises(UnknownTimeZoneError):
73+
with pytest.raises(zoneinfo.ZoneInfoNotFoundError):
8074
Location(32.2, -111, tz)
8175

8276

pvlib/tools.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
"""
44

55
import datetime as dt
6+
import warnings
7+
68
import numpy as np
79
import pandas as pd
810
import pytz
9-
import warnings
1011

1112

1213
def cosd(angle):
@@ -125,7 +126,7 @@ def localize_to_utc(time, location):
125126
----------
126127
time : datetime.datetime, pandas.DatetimeIndex,
127128
or pandas.Series/DataFrame with a DatetimeIndex.
128-
location : pvlib.Location object (unused if time is localized)
129+
location : pvlib.Location object (unused if ``time`` is localized)
129130
130131
Returns
131132
-------

0 commit comments

Comments
 (0)