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
4 changes: 4 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
~~~~~~~~~~~~
* ``Location.pytz`` is deprecated. Use ``Location.tz`` instead.
(:issue:`2343`, :pull:`2757`)


Bug fixes
Expand Down Expand Up @@ -53,3 +55,5 @@ Contributors
~~~~~~~~~~~~
* :ghuser:`Omesh37`
* Cliff Hansen (:ghuser:`cwhanse`)
Comment thread
JoLo90 marked this conversation as resolved.
* :ghuser:`JoLo90`

823 changes: 476 additions & 347 deletions docs/tutorials/solarposition.ipynb
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm okay with keeping just the bare minimal changes to be done in here, but since it's mostly noise, I would revert all changes to it - I won't review this file if there are so many lines changed.

It's obvious that you didn't know it, but these files are no longer maintained nor we expect them to work with latest pvlib versions (I think, please any maintainer correct me if I'm wrong).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@echedey-ls ok I'll just modify the line where the outdated pytz property was used. If it doesn't work anymore, maybe we should delete them.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the ipython files, whatever is easiest from this point onwards. Since the PR submitter updated the workbook, I don't think we need to revert changes.

And I don't recall whether we decided (or just talked about) to not update the notebooks.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cwhanse , yup, it's an overstatement I made. We did remove their reference in docs for being "outdated" thou #2009

I think just cleaning the output of the jupyter execution would be enough to clear the diff and review it.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see the commit history, did you add # noqa: E231 because flake8 was complaining? I may be wrong, but sintax in this line shouldn't raise any flake8 warning. I can't reproduce locally in a reasonable amount of time (flake8 5.0.4 is tech debt at this point...).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@echedey-ls yes it was a false positive, flake8 insisted on putting a space after the :

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.
Comment on lines +26 to 28
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Location objects have a time-zone attribute:
* ``tz`` is an IANA time-zone string.
Location objects have a time-zone attribute ``tz``, 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`` instead.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The ``pytz`` attribute is deprecated. Use ``tz`` instead.
.. deprecated:: 0.15.2
The ``pytz`` attribute is deprecated. Use ``tz`` instead.

Formatting and decor.


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::
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. deprecated::
.. deprecated:: 0.15.2

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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
An error (AmbiguousTimeError in older pandas, ValueError in newer)
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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid the access of the private attribute?
E.g.:

Suggested change
time = time.replace(tzinfo=location._zoneinfo)
time = time.replace(tzinfo=location.tz)

Dunno if it can work like that or needs transforming into another object.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a test that fails in v0.17.0 that asserts a warning is emitted when reading the deprecated attribute?

@fail_on_pvlib_version('0.17.0')
def test_location_pytz_warning():
    with pytest.warns(_deprecation.pvlibDeprecationWarning):
        < a Location instance access to pytz here >

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