Skip to content

Commit 5f59417

Browse files
Support non-fractional int and float and pytz and zoneinfo time zones
1 parent a3c3e03 commit 5f59417

File tree

2 files changed

+106
-44
lines changed

2 files changed

+106
-44
lines changed

pvlib/location.py

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
class Location:
1919
"""
2020
Location objects are convenient containers for latitude, longitude,
21-
timezone, and altitude data associated with a particular
22-
geographic location. You can also assign a name to a location object.
21+
time zone, and altitude data associated with a particular geographic
22+
location. You can also assign a name to a location object.
2323
24-
Location objects have two timezone attributes:
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:
2527
26-
* ``tz`` is a IANA timezone string.
27-
* ``pytz`` is a pytz timezone object.
28+
* ``tz`` is a IANA time-zone string.
29+
* ``pytz`` is a pytz time-zone object.
2830
2931
Location objects support the print method.
3032
@@ -38,12 +40,16 @@ class Location:
3840
Positive is east of the prime meridian.
3941
Use decimal degrees notation.
4042
41-
tz : str, int, float, or pytz.timezone, default 'UTC'.
42-
See
43-
http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
44-
for a list of valid time zones.
45-
pytz.timezone objects will be converted to strings.
46-
ints and floats must be in hours from UTC.
43+
tz : time zone as str, int, float, or datetime.tzinfo (inc. subclasses
44+
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).
50+
Raises TypeError for time zone conversion issues or
51+
pytz.exceptions.UnknownTimeZoneError when (stringified) time zone is
52+
not recognized by pytz.timezone.
4753
4854
altitude : float, optional
4955
Altitude from sea level in meters.
@@ -59,33 +65,17 @@ class Location:
5965
pvlib.pvsystem.PVSystem
6066
"""
6167

62-
def __init__(self, latitude, longitude, tz='UTC', altitude=None,
63-
name=None):
64-
68+
def __init__(
69+
self, latitude, longitude, tz='UTC', altitude=None, name=None
70+
):
6571
self.latitude = latitude
6672
self.longitude = longitude
67-
68-
if isinstance(tz, str):
69-
self.tz = tz
70-
self.pytz = pytz.timezone(tz)
71-
elif isinstance(tz, datetime.timezone):
72-
self.tz = 'UTC'
73-
self.pytz = pytz.UTC
74-
elif isinstance(tz, datetime.tzinfo):
75-
# This includes pytz timezones.
76-
self.tz = tz.zone
77-
self.pytz = pytz.timezone(tz.zone)
78-
elif isinstance(tz, (int, float)):
79-
self.tz = f"Etc/GMT{int(-tz):+d}"
80-
self.pytz = pytz.timezone(self.tz)
81-
else:
82-
raise TypeError('Invalid tz specification')
73+
self.tz = tz
8374

8475
if altitude is None:
8576
altitude = lookup_altitude(latitude, longitude)
8677

8778
self.altitude = altitude
88-
8979
self.name = name
9080

9181
def __repr__(self):
@@ -95,6 +85,35 @@ def __repr__(self):
9585
return ('Location: \n ' + '\n '.join(
9686
f'{attr}: {getattr(self, attr, None)}' for attr in attrs))
9787

88+
@property
89+
def tz(self):
90+
# self.pytz holds the single source of time-zone truth.
91+
return self.pytz.zone
92+
93+
@tz.setter
94+
def tz(self, tz_):
95+
if isinstance(tz_, str):
96+
self.pytz = pytz.timezone(tz_)
97+
elif isinstance(tz_, int):
98+
self.pytz = pytz.timezone(f"Etc/GMT{-tz_:+d}")
99+
elif isinstance(tz_, float):
100+
if tz_ % 1 != 0:
101+
raise TypeError(
102+
"floating point tz does not have zero fractional part: "
103+
f"{tz_}"
104+
)
105+
106+
self.pytz = pytz.timezone(f"Etc/GMT{-int(tz_):+d}")
107+
elif isinstance(tz_, datetime.tzinfo):
108+
# Includes time zones generated by pytz and zoneinfo packages.
109+
self.pytz = pytz.timezone(str(tz_))
110+
else:
111+
raise TypeError(
112+
f"invalid tz specification: {tz_}, must be an IANA time zone "
113+
"string, a non-fractional int/float UTC offset, or a "
114+
"datetime.tzinfo object (including subclasses)"
115+
)
116+
98117
@classmethod
99118
def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs):
100119
"""

pvlib/tests/test_location.py

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
22
from unittest.mock import ANY
3+
import zoneinfo
34

45
import numpy as np
56
from numpy import nan
@@ -27,27 +28,69 @@ def test_location_all():
2728
Location(32.2, -111, 'US/Arizona', 700, 'Tucson')
2829

2930

30-
@pytest.mark.parametrize('tz', [
31-
'America/Phoenix',
32-
datetime.timezone.utc,
33-
pytz.timezone('US/Arizona'),
34-
-7,
35-
-7.0,
36-
])
37-
def test_location_tz(tz):
31+
@pytest.mark.parametrize(
32+
'tz,tz_expected',
33+
[
34+
pytest.param('UTC', 'UTC'),
35+
pytest.param('Etc/GMT+5', 'Etc/GMT+5'),
36+
pytest.param('US/Mountain','US/Mountain'),
37+
pytest.param('America/Phoenix', 'America/Phoenix'),
38+
pytest.param('Asia/Kathmandu', 'Asia/Kathmandu'),
39+
pytest.param('Asia/Yangon', 'Asia/Yangon'),
40+
pytest.param(datetime.timezone.utc, 'UTC'),
41+
pytest.param(zoneinfo.ZoneInfo('Etc/GMT-7'), 'Etc/GMT-7'),
42+
pytest.param(pytz.timezone('US/Arizona'), 'US/Arizona'),
43+
pytest.param(-6, 'Etc/GMT+6'),
44+
pytest.param(-11.0, 'Etc/GMT+11'),
45+
pytest.param(12, 'Etc/GMT-12'),
46+
],
47+
)
48+
def test_location_tz(tz, tz_expected):
3849
loc = Location(32.2, -111, tz)
39-
assert type(loc.tz) is str
50+
assert isinstance(loc.pytz, datetime.tzinfo) # Abstract base class.
4051
assert isinstance(loc.pytz, pytz.tzinfo.BaseTzInfo)
52+
assert type(loc.tz) is str
53+
assert loc.tz == tz_expected
54+
55+
56+
def test_location_tz_update():
57+
loc = Location(32.2, -111, -11)
58+
assert loc.tz == 'Etc/GMT+11'
59+
assert loc.pytz == pytz.timezone('Etc/GMT+11')
60+
61+
# Updating tz updates pytz.
62+
loc.tz = 7
63+
assert loc.tz == 'Etc/GMT-7'
64+
assert loc.pytz == pytz.timezone('Etc/GMT-7')
65+
66+
# Updating pytz updates tz.
67+
loc.pytz = pytz.timezone('US/Arizona')
68+
assert loc.tz == 'US/Arizona'
69+
assert loc.pytz == pytz.timezone('US/Arizona')
4170

4271

43-
def test_location_invalid_tz():
72+
@pytest.mark.parametrize(
73+
'tz', [
74+
'invalid',
75+
'Etc/GMT+20', # offset too large.
76+
20, # offset too large.
77+
]
78+
)
79+
def test_location_invalid_tz(tz):
4480
with pytest.raises(UnknownTimeZoneError):
45-
Location(32.2, -111, 'invalid')
81+
Location(32.2, -111, tz)
4682

4783

48-
def test_location_invalid_tz_type():
84+
@pytest.mark.parametrize(
85+
'tz', [
86+
-9.5, # float with non-zero fractional part.
87+
b"bytes not str",
88+
[5],
89+
]
90+
)
91+
def test_location_invalid_tz_type(tz):
4992
with pytest.raises(TypeError):
50-
Location(32.2, -111, [5])
93+
Location(32.2, -111, tz)
5194

5295

5396
def test_location_print_all():

0 commit comments

Comments
 (0)