Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:

- name: Install Conda environment with Micromamba
if: matrix.environment-type == 'conda'
uses: mamba-org/setup-micromamba@v2
uses: mamba-org/setup-micromamba@v3
with:
environment-file: ${{ env.REQUIREMENTS }}
cache-downloads: false
Expand All @@ -49,6 +49,7 @@ jobs:
python=${{ matrix.python-version }}
condarc: |
channel-priority: flexible
micromamba-version: '2.6.0-0'
env:
# build requirement filename. First replacement is for the python
# version, second is to add "-min" if needed
Expand Down
2 changes: 2 additions & 0 deletions docs/sphinx/source/whatsnew/v0.15.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Breaking Changes

Deprecations
~~~~~~~~~~~~
* Deprecated ``Location.pytz`` attribute. Use ``Location.tz`` property instead.
(:ghuser:`JoLo90`, :pull:`2757`)


Bug fixes
Expand Down
823 changes: 483 additions & 340 deletions docs/tutorials/solarposition.ipynb

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions pvlib/iotools/pvgis.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import requests
import numpy as np
import pandas as pd
import pytz
import zoneinfo
from pvlib.iotools import read_epw

URL = 'https://re.jrc.ec.europa.eu/api/'
Expand Down Expand Up @@ -413,10 +413,10 @@ def _coerce_and_roll_tmy(tmy_data, tz, year):
re-interpreted as zero / UTC.
"""
if tz:
tzname = pytz.timezone(f'Etc/GMT{-tz:+d}')
tzname = zoneinfo.ZoneInfo(f'Etc/GMT{-tz:+d}') # noqa: E231
else:
tz = 0
tzname = pytz.timezone('UTC')
tzname = zoneinfo.ZoneInfo('UTC')
new_index = pd.DatetimeIndex([
timestamp.replace(year=year, tzinfo=tzname)
for timestamp in tmy_data.index],
Expand Down
35 changes: 24 additions & 11 deletions pvlib/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from pvlib import solarposition, clearsky, atmosphere, irradiance
from pvlib.tools import _degrees_to_index
from pvlib._deprecation import warn_deprecated


class Location:
Expand All @@ -22,13 +23,11 @@ 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:
Location objects have a time-zone attribute:

* ``tz`` is an IANA time-zone string.
* ``pytz`` is a pytz-based time-zone object (read only).

The read-only ``pytz`` attribute will stay in sync with any changes made
using ``tz``.
The ``pytz`` attribute is deprecated. Use ``tz`` property instead.

Location objects support the print method.

Expand All @@ -47,8 +46,8 @@ class Location:
list of valid name strings. An `int` or `float` must be a whole-number
hour offsets from UTC that can be converted to the IANA-supported
'Etc/GMT-N' format. (Note the limited range of the offset N and its
sign-change convention.) Time zones from the pytz and zoneinfo packages
may also be passed here, as they are subclasses of datetime.tzinfo.
sign-change convention.) Time zones from the zoneinfo packages may also
be passed here.

The `tz` attribute is represented as a valid IANA time zone name
string.
Expand Down Expand Up @@ -108,17 +107,19 @@ def tz(self, tz_):
if isinstance(tz_, str):
self._zoneinfo = zoneinfo.ZoneInfo(tz_)
elif isinstance(tz_, int):
self._zoneinfo = zoneinfo.ZoneInfo(f"Etc/GMT{-tz_:+d}")
tz_str = f"Etc/GMT{-tz_:+d}" # noqa: E231
self._zoneinfo = zoneinfo.ZoneInfo(tz_str)
elif isinstance(tz_, float):
if tz_ % 1 != 0:
raise TypeError(
"Floating-point tz has non-zero fractional part: "
f"{tz_}. Only whole-number offsets are supported."
)

self._zoneinfo = zoneinfo.ZoneInfo(f"Etc/GMT{-int(tz_):+d}")
tz_str = f"Etc/GMT{-int(tz_):+d}" # noqa: E231
self._zoneinfo = zoneinfo.ZoneInfo(tz_str)
elif isinstance(tz_, datetime.tzinfo):
# Includes time zones generated by pytz and zoneinfo packages.
# Includes time zones generated by zoneinfo packages.
self._zoneinfo = zoneinfo.ZoneInfo(str(tz_))
else:
raise TypeError(
Expand All @@ -128,8 +129,20 @@ def tz(self, tz_):
)

@property
def pytz(self):
"""The location's pytz time zone (read only)."""
def pytz(self): # pragma: no cover
"""The location's pytz time zone (read only).

.. deprecated::
The ``pytz`` attribute is deprecated. Use the ``tz`` property
instead.
"""
warn_deprecated(
since='0.15.2',
removal='0.17.0',
name='pytz',
obj_type='attribute',
alternative='tz',
)
return pytz.timezone(str(self._zoneinfo))

@classmethod
Expand Down
10 changes: 5 additions & 5 deletions pvlib/solarposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -1360,11 +1360,11 @@ def hour_angle(times, longitude, equation_of_time):
Corresponding timestamps, must be localized to the timezone for the
``longitude``.
A `pytz.exceptions.AmbiguousTimeError` will be raised if any of the
given times are on a day when the local daylight savings transition
happens at midnight. If you're working with such a timezone,
consider converting to a non-DST timezone (e.g. GMT-4) before
calling this function.
An error (AmbiguousTimeError in older pandas, ValueError in newer)
will be raised if any of the given times are on a day when the local
daylight savings transition happens at midnight. If you're working
with such a timezone, consider converting to a non-DST timezone
(e.g. GMT-4) before calling this function.
longitude : numeric
Longitude in degrees
equation_of_time : numeric
Expand Down
17 changes: 9 additions & 8 deletions pvlib/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

import contextlib
import datetime as dt
from datetime import timezone
import warnings

import numpy as np
import pandas as pd
import pytz
import zoneinfo


def cosd(angle):
Expand Down Expand Up @@ -135,8 +136,8 @@ def localize_to_utc(time, location):
"""
if isinstance(time, dt.datetime):
if time.tzinfo is None:
time = location.pytz.localize(time)
time_utc = time.astimezone(pytz.utc)
time = time.replace(tzinfo=location._zoneinfo)
time_utc = time.astimezone(timezone.utc)
else:
try:
time_utc = time.tz_convert('UTC')
Expand All @@ -162,11 +163,11 @@ def datetime_to_djd(time):
"""

if time.tzinfo is None:
time_utc = pytz.utc.localize(time)
time_utc = time.replace(tzinfo=timezone.utc)
else:
time_utc = time.astimezone(pytz.utc)
time_utc = time.astimezone(timezone.utc)

djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))
djd_start = dt.datetime(1899, 12, 31, 12, tzinfo=timezone.utc)
djd = (time_utc - djd_start).total_seconds() * 1.0/(60 * 60 * 24)

return djd
Expand All @@ -189,10 +190,10 @@ def djd_to_datetime(djd, tz='UTC'):
The resultant datetime localized to tz
"""

djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))
djd_start = dt.datetime(1899, 12, 31, 12, tzinfo=timezone.utc)

utc_time = djd_start + dt.timedelta(days=djd)
return utc_time.astimezone(pytz.timezone(tz))
return utc_time.astimezone(zoneinfo.ZoneInfo(tz))


def _pandas_to_doy(pd_object):
Expand Down
1 change: 0 additions & 1 deletion tests/iotools/test_midc.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import pandas as pd
import pytest
import pytz

from pvlib.iotools import midc
from tests.conftest import TESTS_DATA_DIR, RERUNS, RERUNS_DELAY
Expand Down
4 changes: 2 additions & 2 deletions tests/test_clearsky.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import numpy as np
from numpy import nan
import pandas as pd
import pytz
import zoneinfo
from scipy.linalg import hankel

import pytest
Expand Down Expand Up @@ -770,7 +770,7 @@ def test_bird():
times = pd.date_range(start='1/1/2015 0:00', end='12/31/2015 23:00',
freq='h')
tz = -7 # test timezone
gmt_tz = pytz.timezone('Etc/GMT%+d' % -(tz))
gmt_tz = zoneinfo.ZoneInfo(f'Etc/GMT{-tz:+d}') # noqa: E231
times = times.tz_localize(gmt_tz) # set timezone
times_utc = times.tz_convert('UTC')
# match test data from BIRD_08_16_2012.xls
Expand Down
13 changes: 4 additions & 9 deletions tests/test_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@

import pytest

import pytz

import pvlib
from pvlib import location
from pvlib.location import Location, lookup_altitude
Expand All @@ -37,29 +35,26 @@ def test_location_all():
pytest.param('Asia/Yangon', 'Asia/Yangon'),
pytest.param(datetime.timezone.utc, 'UTC'),
pytest.param(zoneinfo.ZoneInfo('Etc/GMT-7'), 'Etc/GMT-7'),
pytest.param(pytz.timezone('US/Arizona'), 'US/Arizona'),
pytest.param(zoneinfo.ZoneInfo('US/Arizona'), 'US/Arizona'),
pytest.param(-6, 'Etc/GMT+6'),
pytest.param(-11.0, 'Etc/GMT+11'),
pytest.param(12, 'Etc/GMT-12'),
],
)
def test_location_tz(tz, tz_expected):
loc = Location(32.2, -111, tz)
assert isinstance(loc.pytz, datetime.tzinfo) # Abstract base class.
assert isinstance(loc.pytz, pytz.tzinfo.BaseTzInfo)
assert isinstance(loc._zoneinfo, datetime.tzinfo) # Abstract base class.
assert type(loc.tz) is str
assert loc.tz == tz_expected


def test_location_tz_update():
loc = Location(32.2, -111, -11)
assert loc.tz == 'Etc/GMT+11'
assert loc.pytz == pytz.timezone('Etc/GMT+11') # Deprecated attribute.

# Updating Location's tz updates read-only time-zone attributes.
loc.tz = 7
assert loc.tz == 'Etc/GMT-7'
assert loc.pytz == pytz.timezone('Etc/GMT-7') # Deprecated attribute.


@pytest.mark.parametrize(
Expand Down Expand Up @@ -99,8 +94,8 @@ def test_location_print_all():
assert tus.__str__() == expected_str


def test_location_print_pytz():
tus = Location(32.2, -111, pytz.timezone('US/Arizona'), 700, 'Tucson')
def test_location_print():
tus = Location(32.2, -111, zoneinfo.ZoneInfo('US/Arizona'), 700, 'Tucson')
expected_str = '\n'.join([
'Location: ',
' name: Tucson',
Expand Down
47 changes: 26 additions & 21 deletions tests/test_solarposition.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import calendar
import datetime
import math
import warnings
import zoneinfo
from datetime import timezone

import numpy as np
import pandas as pd

from .conftest import assert_frame_equal, assert_series_equal
from numpy.testing import assert_allclose
import pytest
import pytz
from numpy.testing import assert_allclose

from pvlib.location import Location
from pvlib import solarposition, spa
from pvlib.location import Location

from .conftest import (
requires_ephem, requires_spa_c, requires_numba, requires_pandas_2_0
)
from .conftest import (assert_frame_equal, assert_series_equal, requires_ephem,
requires_numba, requires_pandas_2_0, requires_spa_c)

# setup times and locations to be tested.
times = pd.date_range(start=datetime.datetime(2014, 6, 24),
Expand Down Expand Up @@ -343,29 +342,26 @@ def test_pyephem_physical_dst(expected_solpos, golden):

@requires_ephem
def test_calc_time():
import pytz
import math
# validation from USNO solar position calculator online

epoch = datetime.datetime(1970, 1, 1)
epoch_dt = pytz.utc.localize(epoch)
epoch = datetime.datetime(1970, 1, 1, tzinfo=timezone.utc)

loc = tus
loc.pressure = 0
actual_time = pytz.timezone(loc.tz).localize(
datetime.datetime(2014, 10, 10, 8, 30))
lb = pytz.timezone(loc.tz).localize(datetime.datetime(2014, 10, 10, tol))
ub = pytz.timezone(loc.tz).localize(datetime.datetime(2014, 10, 10, 10))
tz = zoneinfo.ZoneInfo(loc.tz)
actual_time = datetime.datetime(2014, 10, 10, 8, 30, tzinfo=tz)
lb = datetime.datetime(2014, 10, 10, tol, tzinfo=tz)
ub = datetime.datetime(2014, 10, 10, 10, tzinfo=tz)
alt = solarposition.calc_time(lb, ub, loc.latitude, loc.longitude,
'alt', math.radians(24.7))
az = solarposition.calc_time(lb, ub, loc.latitude, loc.longitude,
'az', math.radians(116.3))
actual_timestamp = (actual_time - epoch_dt).total_seconds()
actual_timestamp = (actual_time - epoch).total_seconds()

assert_allclose((alt.replace(second=0, microsecond=0) -
epoch_dt).total_seconds(), actual_timestamp)
epoch).total_seconds(), actual_timestamp)
assert_allclose((az.replace(second=0, microsecond=0) -
epoch_dt).total_seconds(), actual_timestamp)
epoch).total_seconds(), actual_timestamp)


@requires_ephem
Expand Down Expand Up @@ -715,6 +711,15 @@ def test_hour_angle_with_tricky_timezones():
# GH 2132
# tests timezones that have a DST shift at midnight

try: # transitive dependency to pytz through pandas < 3
import pytz
_NonExistentTimeError = pytz.exceptions.NonExistentTimeError
_AmbiguousTimeError = pytz.exceptions.AmbiguousTimeError
except ImportError: # pragma: no cover
# pandas 3.x dropped pytz; these are now raised as ValueError
_NonExistentTimeError = ValueError
_AmbiguousTimeError = ValueError

eot = np.array([-3.935172, -4.117227, -4.026295, -4.026295])

longitude = 70.6693
Expand All @@ -726,7 +731,7 @@ def test_hour_angle_with_tricky_timezones():
]).tz_localize('America/Santiago', nonexistent='shift_forward')

with pytest.raises((
pytz.exceptions.NonExistentTimeError, # pandas 1.x, 2.x
_NonExistentTimeError, # pandas 1.x, 2.x
ValueError, # pandas 3.x
)):
times.normalize()
Expand All @@ -743,7 +748,7 @@ def test_hour_angle_with_tricky_timezones():
]).tz_localize('America/Havana', ambiguous=[True, True, False, False])

with pytest.raises((
pytz.exceptions.AmbiguousTimeError, # pandas 1.x, 2.x
_AmbiguousTimeError, # pandas 1.x, 2.x
ValueError, # pandas 3.x
)):
solarposition.hour_angle(times, longitude, eot)
Expand Down
Loading