Skip to content

Commit

Permalink
Use zoneinfo as single source of truth and tz as interface point
Browse files Browse the repository at this point in the history
  • Loading branch information
markcampanelli committed Jan 10, 2025
1 parent c84801f commit 195efbc
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 30 deletions.
56 changes: 35 additions & 21 deletions pvlib/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import pathlib
import datetime
import zoneinfo

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

@property
def tz(self):
# self.pytz holds the single source of time-zone truth.
return self.pytz.zone
return str(self._zoneinfo)

@tz.setter
def tz(self, tz_):
# self._zoneinfo holds single source of time-zone truth as IANA name.
if isinstance(tz_, str):
self.pytz = pytz.timezone(tz_)
self._zoneinfo = zoneinfo.ZoneInfo(tz_)
elif isinstance(tz_, int):
self.pytz = pytz.timezone(f"Etc/GMT{-tz_:+d}")
self._zoneinfo = zoneinfo.ZoneInfo(f"Etc/GMT{-tz_:+d}")
elif isinstance(tz_, float):
if tz_ % 1 != 0:
raise TypeError(
"floating point tz does not have zero fractional part: "
f"{tz_}"
"Floating-point tz has non-zero fractional part: "
f"{tz_}. Only whole-number offsets are supported."
)

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

@property
def pytz(self):
return pytz.timezone(str(self._zoneinfo))

@property
def zoneinfo(self):
return self._zoneinfo

@classmethod
def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs):
"""
Expand Down
8 changes: 1 addition & 7 deletions pvlib/tests/test_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import pytest

import pytz
from pytz.exceptions import UnknownTimeZoneError

import pvlib
from pvlib import location
Expand Down Expand Up @@ -62,11 +61,6 @@ def test_location_tz_update():
assert loc.tz == 'Etc/GMT-7'
assert loc.pytz == pytz.timezone('Etc/GMT-7')

# Updating pytz updates tz.
loc.pytz = pytz.timezone('US/Arizona')
assert loc.tz == 'US/Arizona'
assert loc.pytz == pytz.timezone('US/Arizona')


@pytest.mark.parametrize(
'tz', [
Expand All @@ -76,7 +70,7 @@ def test_location_tz_update():
]
)
def test_location_invalid_tz(tz):
with pytest.raises(UnknownTimeZoneError):
with pytest.raises(zoneinfo.ZoneInfoNotFoundError):
Location(32.2, -111, tz)


Expand Down
5 changes: 3 additions & 2 deletions pvlib/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
"""

import datetime as dt
import warnings

import numpy as np
import pandas as pd
import pytz
import warnings


def cosd(angle):
Expand Down Expand Up @@ -125,7 +126,7 @@ def localize_to_utc(time, location):
----------
time : datetime.datetime, pandas.DatetimeIndex,
or pandas.Series/DataFrame with a DatetimeIndex.
location : pvlib.Location object (unused if time is localized)
location : pvlib.Location object (unused if ``time`` is localized)
Returns
-------
Expand Down

0 comments on commit 195efbc

Please sign in to comment.