From 9347f125540217b68dafa7d46d54941b399d6875 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Sun, 22 Dec 2024 18:51:55 -0700 Subject: [PATCH 01/42] Add tests for tools.localize_to_utc --- pvlib/tests/test_tools.py | 140 +++++++++++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index eb9e65c895..4321d3c52b 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -1,9 +1,12 @@ -import pytest +from datetime import datetime +from zoneinfo import ZoneInfo -from pvlib import tools import numpy as np -import pandas as pd from numpy.testing import assert_allclose +import pandas as pd +import pytest + +from pvlib import location, tools @pytest.mark.parametrize('keys, input_dict, expected', [ @@ -144,3 +147,134 @@ def test_get_pandas_index(args, args_idx): def test_normalize_max2one(data_in, expected): result = tools.normalize_max2one(data_in) assert_allclose(result, expected) + + +@pytest.mark.parametrize( + 'input,expected', + [ + ( + { + "time": datetime( + 1974, 6, 22, 18, 30, 15, tzinfo=ZoneInfo("Etc/GMT+5"), + ), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + datetime(1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC")), + ), + ( + { + "time": datetime(1974, 6, 22, 18, 30, 15), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + datetime(1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC")), + ), + ( + { + "time": pd.DatetimeIndex( + ["1974-06-22T18:30:15"], + tz=ZoneInfo("Etc/GMT+5"), + ), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), + ), + ( + { + "time": pd.DatetimeIndex(["1974-06-22T18:30:15"]), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), + ), + ( + { + "time": pd.Series( + [24.42], + index=pd.DatetimeIndex( + ["1974-06-22T18:30:15"], + tz=ZoneInfo("Etc/GMT+5"), + ), + ), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + pd.Series( + [24.42], + pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), + ), + ), + ( + { + "time": pd.Series( + [24.42], + index=pd.DatetimeIndex(["1974-06-22T18:30:15"]), + ), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + pd.Series( + [24.42], + pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), + ), + ), + ( + { + "time": pd.DataFrame( + [[24.42]], + index=pd.DatetimeIndex( + ["1974-06-22T18:30:15"], + tz=ZoneInfo("Etc/GMT+5"), + ), + ), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + pd.DataFrame( + [[24.42]], + pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), + ), + ), + ( + { + "time": pd.DataFrame( + [[24.42]], + index=pd.DatetimeIndex(["1974-06-22T18:30:15"]), + ), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + pd.DataFrame( + [[24.42]], + pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), + ), + ), + ], + ids=[ + "datetime.datetime with tzinfo", + "datetime.datetime", + "pandas.DatetimeIndex with tzinfo", + "pandas.DatetimeIndex", + "pandas.Series with tzinfo", + "pandas.Series", + "pandas.DataFrame with tzinfo", + "pandas.DataFrame", + ], +) +def test_localize_to_utc(input, expected): + """Test localization of naive time to UTC using the specified location.""" + got = tools.localize_to_utc(**input) + if isinstance(got, (pd.Series, pd.DataFrame)): + assert got.equals(expected) + else: + assert got == expected From 06387734756824547bbc9ecc93e63edd8dfc16a6 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Sun, 22 Dec 2024 21:52:42 -0700 Subject: [PATCH 02/42] Add tests for datetime_to_djd and djd_to_datetime --- pvlib/tests/test_tools.py | 58 ++++++++++++++++++++++++++++++++++++++- pvlib/tools.py | 4 +-- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 4321d3c52b..4ad96eb830 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -272,9 +272,65 @@ def test_normalize_max2one(data_in, expected): ], ) def test_localize_to_utc(input, expected): - """Test localization of naive time to UTC using the specified location.""" got = tools.localize_to_utc(**input) if isinstance(got, (pd.Series, pd.DataFrame)): assert got.equals(expected) else: assert got == expected + + +@pytest.mark.parametrize( + 'input,expected', + [ + ( + { + "time": datetime(1974, 6, 22, 18, 30, 15, tzinfo=ZoneInfo("Etc/GMT+5")) + }, + 27201.47934027778, + ), + ( + { + "time": datetime(1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC")) + }, + 27201.47934027778, + ), + ], + ids=[ + "datetime.datetime with tzinfo", + "datetime.datetime", + ], +) +def test_datetime_to_djd(input, expected): + assert tools.datetime_to_djd(input["time"]) == expected + + +@pytest.mark.parametrize( + 'input,expected', + [ + ( + { + "djd": 27201.47934027778, + "tz": "Etc/GMT+5", + }, + datetime(1974, 6, 22, 18, 30, 15, tzinfo=ZoneInfo("Etc/GMT+5")), + ), + ( + { + "djd": 27201.47934027778, + "tz": None, + }, + datetime(1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC")), + ), + ], + ids=[ + "djd with tzinfo", + "djd", + ], +) +def test_djd_to_datetime(input, expected): + if input["tz"] is not None: + got = tools.djd_to_datetime(input["djd"]) + else: + got = tools.djd_to_datetime(input["djd"], tz="Etc/GMT+5") + + assert got == expected diff --git a/pvlib/tools.py b/pvlib/tools.py index c8d4d6e309..86c2df937d 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -119,7 +119,7 @@ def atand(number): def localize_to_utc(time, location): """ - Converts or localizes a time series to UTC. + Converts time to UTC, localizing if necessary using location. Parameters ---------- @@ -129,7 +129,7 @@ def localize_to_utc(time, location): Returns ------- - pandas object localized to UTC. + datetime.datetime or pandas object localized to UTC. """ if isinstance(time, dt.datetime): if time.tzinfo is None: From c1df9a755e6cb82137dafc2fa6ff4f8c0cc11279 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Sun, 22 Dec 2024 22:04:48 -0700 Subject: [PATCH 03/42] Update what's new --- docs/sphinx/source/whatsnew/v0.11.3.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.3.rst b/docs/sphinx/source/whatsnew/v0.11.3.rst index 238ec1e9a6..82ccaa2381 100644 --- a/docs/sphinx/source/whatsnew/v0.11.3.rst +++ b/docs/sphinx/source/whatsnew/v0.11.3.rst @@ -18,6 +18,7 @@ Documentation Testing ~~~~~~~ +* Add tests for time conversions in tools package. (:issue:`2340`, :pull:`2341`) Requirements @@ -26,5 +27,4 @@ Requirements Contributors ~~~~~~~~~~~~ - - +* Mark Campanellli (:ghuser:`markcampanelli`) From 77c0f81856bdd8742e3529e82d962b6118f18139 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Sun, 22 Dec 2024 22:09:50 -0700 Subject: [PATCH 04/42] Appease the linter --- pvlib/tests/test_tools.py | 266 +++++++++++++++++++------------------- 1 file changed, 132 insertions(+), 134 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 4ad96eb830..6c3e6243f0 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -150,116 +150,116 @@ def test_normalize_max2one(data_in, expected): @pytest.mark.parametrize( - 'input,expected', - [ - ( - { - "time": datetime( - 1974, 6, 22, 18, 30, 15, tzinfo=ZoneInfo("Etc/GMT+5"), - ), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - datetime(1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC")), - ), - ( - { - "time": datetime(1974, 6, 22, 18, 30, 15), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - datetime(1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC")), - ), - ( - { - "time": pd.DatetimeIndex( + 'input,expected', + [ + ( + { + "time": datetime( + 1974, 6, 22, 18, 30, 15, tzinfo=ZoneInfo("Etc/GMT+5"), + ), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + datetime(1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC")), + ), + ( + { + "time": datetime(1974, 6, 22, 18, 30, 15), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + datetime(1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC")), + ), + ( + { + "time": pd.DatetimeIndex( + ["1974-06-22T18:30:15"], + tz=ZoneInfo("Etc/GMT+5"), + ), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), + ), + ( + { + "time": pd.DatetimeIndex(["1974-06-22T18:30:15"]), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), + ), + ( + { + "time": pd.Series( + [24.42], + index=pd.DatetimeIndex( ["1974-06-22T18:30:15"], tz=ZoneInfo("Etc/GMT+5"), ), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), - ), - ( - { - "time": pd.DatetimeIndex(["1974-06-22T18:30:15"]), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), - ), - ( - { - "time": pd.Series( - [24.42], - index=pd.DatetimeIndex( - ["1974-06-22T18:30:15"], - tz=ZoneInfo("Etc/GMT+5"), - ), - ), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - pd.Series( - [24.42], - pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), ), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + pd.Series( + [24.42], + pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), ), - ( - { - "time": pd.Series( - [24.42], - index=pd.DatetimeIndex(["1974-06-22T18:30:15"]), - ), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - pd.Series( + ), + ( + { + "time": pd.Series( [24.42], - pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), + index=pd.DatetimeIndex(["1974-06-22T18:30:15"]), ), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + pd.Series( + [24.42], + pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), ), - ( - { - "time": pd.DataFrame( - [[24.42]], - index=pd.DatetimeIndex( - ["1974-06-22T18:30:15"], - tz=ZoneInfo("Etc/GMT+5"), - ), - ), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - pd.DataFrame( + ), + ( + { + "time": pd.DataFrame( [[24.42]], - pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), + index=pd.DatetimeIndex( + ["1974-06-22T18:30:15"], + tz=ZoneInfo("Etc/GMT+5"), + ), ), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + pd.DataFrame( + [[24.42]], + pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), ), - ( - { - "time": pd.DataFrame( - [[24.42]], - index=pd.DatetimeIndex(["1974-06-22T18:30:15"]), - ), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - pd.DataFrame( + ), + ( + { + "time": pd.DataFrame( [[24.42]], - pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), + index=pd.DatetimeIndex(["1974-06-22T18:30:15"]), ), + "location": location.Location( + 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" + ) + }, + pd.DataFrame( + [[24.42]], + pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), ), - ], + ), + ], ids=[ "datetime.datetime with tzinfo", "datetime.datetime", @@ -280,52 +280,50 @@ def test_localize_to_utc(input, expected): @pytest.mark.parametrize( - 'input,expected', - [ - ( - { - "time": datetime(1974, 6, 22, 18, 30, 15, tzinfo=ZoneInfo("Etc/GMT+5")) - }, - 27201.47934027778, - ), - ( - { - "time": datetime(1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC")) - }, - 27201.47934027778, - ), - ], - ids=[ - "datetime.datetime with tzinfo", - "datetime.datetime", + 'input,expected', + [ + ( + { + "time": datetime( + 1974, 6, 22, 18, 30, 15, tzinfo=ZoneInfo("Etc/GMT+5") + ) + }, + 27201.47934027778, + ), + ( + { + "time": datetime( + 1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC") + ) + }, + 27201.47934027778, + ), ], + ids=["datetime.datetime with tzinfo", "datetime.datetime"], ) def test_datetime_to_djd(input, expected): assert tools.datetime_to_djd(input["time"]) == expected @pytest.mark.parametrize( - 'input,expected', - [ - ( - { - "djd": 27201.47934027778, - "tz": "Etc/GMT+5", - }, - datetime(1974, 6, 22, 18, 30, 15, tzinfo=ZoneInfo("Etc/GMT+5")), - ), - ( - { - "djd": 27201.47934027778, - "tz": None, - }, - datetime(1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC")), - ), - ], - ids=[ - "djd with tzinfo", - "djd", + 'input,expected', + [ + ( + { + "djd": 27201.47934027778, + "tz": "Etc/GMT+5", + }, + datetime(1974, 6, 22, 18, 30, 15, tzinfo=ZoneInfo("Etc/GMT+5")), + ), + ( + { + "djd": 27201.47934027778, + "tz": None, + }, + datetime(1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC")), + ), ], + ids=["djd with tzinfo", "djd"], ) def test_djd_to_datetime(input, expected): if input["tz"] is not None: From 6704d069ebeefe826c492d82575847a09a0e8f78 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Sun, 22 Dec 2024 22:18:05 -0700 Subject: [PATCH 05/42] Fix pandas equality tests for Python 3.9 --- pvlib/tests/test_tools.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 6c3e6243f0..538c73e9c1 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -273,8 +273,11 @@ def test_normalize_max2one(data_in, expected): ) def test_localize_to_utc(input, expected): got = tools.localize_to_utc(**input) - if isinstance(got, (pd.Series, pd.DataFrame)): - assert got.equals(expected) + + if isinstance(got, pd.Series): + pd.testing.assert_series_equal(got, expected) + elif isinstance(got, pd.DataFrame): + pd.testing.assert_frame_equal(got, expected) else: assert got == expected From dbb18055ede0b8d2628b59017bffedb64a96aa90 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Sun, 22 Dec 2024 22:35:42 -0700 Subject: [PATCH 06/42] Fix pandas equality tests for Python 3.9 more --- pvlib/tests/test_tools.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 538c73e9c1..96e4ae6a42 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -1,3 +1,4 @@ +import sys from datetime import datetime from zoneinfo import ZoneInfo @@ -272,12 +273,18 @@ def test_normalize_max2one(data_in, expected): ], ) def test_localize_to_utc(input, expected): + # Pandas has bad dtype check in Python 3.9. + if (sys.version_info[0], sys.version_info[1]) == (3, 9): + check_dtype = False + else: + check_dtype = True + got = tools.localize_to_utc(**input) if isinstance(got, pd.Series): - pd.testing.assert_series_equal(got, expected) + pd.testing.assert_series_equal(got, expected, check_dtype=check_dtype) elif isinstance(got, pd.DataFrame): - pd.testing.assert_frame_equal(got, expected) + pd.testing.assert_frame_equal(got, expected, check_dtype=check_dtype) else: assert got == expected From 67507095923aceb3d8d325ce4f93d380e41ede72 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Sun, 22 Dec 2024 22:43:35 -0700 Subject: [PATCH 07/42] Fix pandas equality tests for Python 3.9 more more --- pvlib/tests/test_tools.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 96e4ae6a42..980cdac831 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -1,4 +1,3 @@ -import sys from datetime import datetime from zoneinfo import ZoneInfo @@ -273,18 +272,12 @@ def test_normalize_max2one(data_in, expected): ], ) def test_localize_to_utc(input, expected): - # Pandas has bad dtype check in Python 3.9. - if (sys.version_info[0], sys.version_info[1]) == (3, 9): - check_dtype = False - else: - check_dtype = True - got = tools.localize_to_utc(**input) - if isinstance(got, pd.Series): - pd.testing.assert_series_equal(got, expected, check_dtype=check_dtype) - elif isinstance(got, pd.DataFrame): - pd.testing.assert_frame_equal(got, expected, check_dtype=check_dtype) + # Pandas has wonky dtype check in Python 3.9. + if isinstance(got, (pd.Series, pd.DataFrame)): + pd.testing.assert_index_equal(got.index, expected.index) + np.testing.assert_array_equal(got.to_numpy(), expected.to_numpy()) else: assert got == expected From 1144106e49bcd2c1d1788963425e92a948a284ef Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Sun, 22 Dec 2024 22:51:32 -0700 Subject: [PATCH 08/42] Bump miniimum pandas to fix bad test failure --- benchmarks/asv.conf.json | 2 +- ci/requirements-py3.10.yml | 2 +- ci/requirements-py3.11.yml | 2 +- ci/requirements-py3.12.yml | 2 +- ci/requirements-py3.9-min.yml | 2 +- ci/requirements-py3.9.yml | 2 +- docs/sphinx/source/whatsnew/v0.11.3.rst | 1 + pyproject.toml | 2 +- 8 files changed, 8 insertions(+), 7 deletions(-) diff --git a/benchmarks/asv.conf.json b/benchmarks/asv.conf.json index 62a21bf505..58a95f6911 100644 --- a/benchmarks/asv.conf.json +++ b/benchmarks/asv.conf.json @@ -116,7 +116,7 @@ "python": "3.9", "build": "", "numpy": "1.19.5", - "pandas": "1.3.0", + "pandas": "1.4.0", "scipy": "1.6.0", // Note: these don't have a minimum in setup.py "h5py": "3.1.0", diff --git a/ci/requirements-py3.10.yml b/ci/requirements-py3.10.yml index fcb89d2e7f..a4593630c3 100644 --- a/ci/requirements-py3.10.yml +++ b/ci/requirements-py3.10.yml @@ -9,7 +9,7 @@ dependencies: - h5py - numba - numpy >= 1.17.3 - - pandas >= 1.3.0 + - pandas >= 1.4.0 - pip - pytest - pytest-cov diff --git a/ci/requirements-py3.11.yml b/ci/requirements-py3.11.yml index f6556ecf94..862a4c380e 100644 --- a/ci/requirements-py3.11.yml +++ b/ci/requirements-py3.11.yml @@ -9,7 +9,7 @@ dependencies: - h5py - numba - numpy >= 1.17.3 - - pandas >= 1.3.0 + - pandas >= 1.4.0 - pip - pytest - pytest-cov diff --git a/ci/requirements-py3.12.yml b/ci/requirements-py3.12.yml index f3d8fc2d0c..e898feefba 100644 --- a/ci/requirements-py3.12.yml +++ b/ci/requirements-py3.12.yml @@ -9,7 +9,7 @@ dependencies: - h5py - numba - numpy >= 1.17.3 - - pandas >= 1.3.0 + - pandas >= 1.4.0 - pip - pytest - pytest-cov diff --git a/ci/requirements-py3.9-min.yml b/ci/requirements-py3.9-min.yml index d17df337fd..311adef686 100644 --- a/ci/requirements-py3.9-min.yml +++ b/ci/requirements-py3.9-min.yml @@ -14,7 +14,7 @@ dependencies: - pip: - h5py==3.0.0 - numpy==1.19.3 - - pandas==1.3.0 # min version of pvlib + - pandas==1.4.0 # min version of pvlib - scipy==1.6.0 - pytest-rerunfailures # conda version is >3.6 - pytest-remotedata # conda package is 0.3.0, needs > 0.3.1 diff --git a/ci/requirements-py3.9.yml b/ci/requirements-py3.9.yml index b5aa976b4b..f1b2503eac 100644 --- a/ci/requirements-py3.9.yml +++ b/ci/requirements-py3.9.yml @@ -9,7 +9,7 @@ dependencies: - h5py - numba - numpy >= 1.17.3 - - pandas >= 1.3.0 + - pandas >= 1.4.0 - pip - pytest - pytest-cov diff --git a/docs/sphinx/source/whatsnew/v0.11.3.rst b/docs/sphinx/source/whatsnew/v0.11.3.rst index 82ccaa2381..fca9ffde23 100644 --- a/docs/sphinx/source/whatsnew/v0.11.3.rst +++ b/docs/sphinx/source/whatsnew/v0.11.3.rst @@ -23,6 +23,7 @@ Testing Requirements ~~~~~~~~~~~~ +* Minimum version of pandas advanced from 1.3.0 to 1.4.0. (:pull:`2341`) Contributors diff --git a/pyproject.toml b/pyproject.toml index 8464b7ce23..b40a5d6191 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ authors = [ requires-python = ">=3.9" dependencies = [ 'numpy >= 1.19.3', - 'pandas >= 1.3.0', + 'pandas >= 1.4.0', 'pytz', 'requests', 'scipy >= 1.6.0', From 14715ed5ae884ea834e561286dcbca80517f53f2 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Sun, 22 Dec 2024 23:01:44 -0700 Subject: [PATCH 09/42] Try alternative pandas test fix --- pvlib/tests/test_tools.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 980cdac831..b4d3ab928e 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -274,9 +274,11 @@ def test_normalize_max2one(data_in, expected): def test_localize_to_utc(input, expected): got = tools.localize_to_utc(**input) - # Pandas has wonky dtype check in Python 3.9. if isinstance(got, (pd.Series, pd.DataFrame)): - pd.testing.assert_index_equal(got.index, expected.index) + # Older pandas versions have wonky dtype check on index. + for index_got, index_expected in zip(got.index, expected.index): + assert index_got == index_expected + np.testing.assert_array_equal(got.to_numpy(), expected.to_numpy()) else: assert got == expected From 545c1966dc56e86e8082eab09334330322290112 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Sun, 22 Dec 2024 23:10:23 -0700 Subject: [PATCH 10/42] Revert change in minimum pandas version --- benchmarks/asv.conf.json | 2 +- ci/requirements-py3.10.yml | 2 +- ci/requirements-py3.11.yml | 2 +- ci/requirements-py3.12.yml | 2 +- ci/requirements-py3.9-min.yml | 2 +- ci/requirements-py3.9.yml | 2 +- docs/sphinx/source/whatsnew/v0.11.3.rst | 1 - pvlib/tests/test_tools.py | 7 ++++--- pyproject.toml | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/benchmarks/asv.conf.json b/benchmarks/asv.conf.json index 58a95f6911..62a21bf505 100644 --- a/benchmarks/asv.conf.json +++ b/benchmarks/asv.conf.json @@ -116,7 +116,7 @@ "python": "3.9", "build": "", "numpy": "1.19.5", - "pandas": "1.4.0", + "pandas": "1.3.0", "scipy": "1.6.0", // Note: these don't have a minimum in setup.py "h5py": "3.1.0", diff --git a/ci/requirements-py3.10.yml b/ci/requirements-py3.10.yml index a4593630c3..fcb89d2e7f 100644 --- a/ci/requirements-py3.10.yml +++ b/ci/requirements-py3.10.yml @@ -9,7 +9,7 @@ dependencies: - h5py - numba - numpy >= 1.17.3 - - pandas >= 1.4.0 + - pandas >= 1.3.0 - pip - pytest - pytest-cov diff --git a/ci/requirements-py3.11.yml b/ci/requirements-py3.11.yml index 862a4c380e..f6556ecf94 100644 --- a/ci/requirements-py3.11.yml +++ b/ci/requirements-py3.11.yml @@ -9,7 +9,7 @@ dependencies: - h5py - numba - numpy >= 1.17.3 - - pandas >= 1.4.0 + - pandas >= 1.3.0 - pip - pytest - pytest-cov diff --git a/ci/requirements-py3.12.yml b/ci/requirements-py3.12.yml index e898feefba..f3d8fc2d0c 100644 --- a/ci/requirements-py3.12.yml +++ b/ci/requirements-py3.12.yml @@ -9,7 +9,7 @@ dependencies: - h5py - numba - numpy >= 1.17.3 - - pandas >= 1.4.0 + - pandas >= 1.3.0 - pip - pytest - pytest-cov diff --git a/ci/requirements-py3.9-min.yml b/ci/requirements-py3.9-min.yml index 311adef686..d17df337fd 100644 --- a/ci/requirements-py3.9-min.yml +++ b/ci/requirements-py3.9-min.yml @@ -14,7 +14,7 @@ dependencies: - pip: - h5py==3.0.0 - numpy==1.19.3 - - pandas==1.4.0 # min version of pvlib + - pandas==1.3.0 # min version of pvlib - scipy==1.6.0 - pytest-rerunfailures # conda version is >3.6 - pytest-remotedata # conda package is 0.3.0, needs > 0.3.1 diff --git a/ci/requirements-py3.9.yml b/ci/requirements-py3.9.yml index f1b2503eac..b5aa976b4b 100644 --- a/ci/requirements-py3.9.yml +++ b/ci/requirements-py3.9.yml @@ -9,7 +9,7 @@ dependencies: - h5py - numba - numpy >= 1.17.3 - - pandas >= 1.4.0 + - pandas >= 1.3.0 - pip - pytest - pytest-cov diff --git a/docs/sphinx/source/whatsnew/v0.11.3.rst b/docs/sphinx/source/whatsnew/v0.11.3.rst index fca9ffde23..82ccaa2381 100644 --- a/docs/sphinx/source/whatsnew/v0.11.3.rst +++ b/docs/sphinx/source/whatsnew/v0.11.3.rst @@ -23,7 +23,6 @@ Testing Requirements ~~~~~~~~~~~~ -* Minimum version of pandas advanced from 1.3.0 to 1.4.0. (:pull:`2341`) Contributors diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index b4d3ab928e..eec22fad14 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -275,11 +275,12 @@ def test_localize_to_utc(input, expected): got = tools.localize_to_utc(**input) if isinstance(got, (pd.Series, pd.DataFrame)): - # Older pandas versions have wonky dtype check on index. + # Older pandas versions have wonky dtype equality check on timestamp + # index, so check the values as numpy.ndarray and indices one by one. + np.testing.assert_array_equal(got.to_numpy(), expected.to_numpy()) + for index_got, index_expected in zip(got.index, expected.index): assert index_got == index_expected - - np.testing.assert_array_equal(got.to_numpy(), expected.to_numpy()) else: assert got == expected diff --git a/pyproject.toml b/pyproject.toml index b40a5d6191..8464b7ce23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ authors = [ requires-python = ">=3.9" dependencies = [ 'numpy >= 1.19.3', - 'pandas >= 1.4.0', + 'pandas >= 1.3.0', 'pytz', 'requests', 'scipy >= 1.6.0', From 271fd974973e5bd4b9e5af1ba6a5be406da94752 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 23 Dec 2024 00:17:05 -0700 Subject: [PATCH 11/42] Fix test --- pvlib/tests/test_tools.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index eec22fad14..1b5eb63c55 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -298,9 +298,7 @@ def test_localize_to_utc(input, expected): ), ( { - "time": datetime( - 1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC") - ) + "time": datetime(1974, 6, 22, 23, 30, 15) }, 27201.47934027778, ), From 01263c2155e48ca586d6430e4f9674e0755ca181 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 23 Dec 2024 14:40:25 -0700 Subject: [PATCH 12/42] Type Location's tz and pytz attributes as advertised --- pvlib/location.py | 10 ++++++---- pvlib/tools.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index 9259f410fa..e3ad7e6db1 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -73,10 +73,10 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=None, self.pytz = pytz.UTC elif isinstance(tz, datetime.tzinfo): self.tz = tz.zone - self.pytz = tz + self.pytz = pytz.timezone(tz.zone) elif isinstance(tz, (int, float)): - self.tz = tz - self.pytz = pytz.FixedOffset(tz*60) + self.tz = f"Etc/GMT{int(-tz):+d}" + self.pytz = pytz.timezone(self.tz) else: raise TypeError('Invalid tz specification') @@ -89,8 +89,10 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=None, def __repr__(self): attrs = ['name', 'latitude', 'longitude', 'altitude', 'tz'] + # Use None as getattr default in case __repr__ is called during + # initialization before all attributes have been assigned. return ('Location: \n ' + '\n '.join( - f'{attr}: {getattr(self, attr)}' for attr in attrs)) + f'{attr}: {getattr(self, attr, None)}' for attr in attrs)) @classmethod def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs): diff --git a/pvlib/tools.py b/pvlib/tools.py index 86c2df937d..535b197a25 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -133,7 +133,7 @@ def localize_to_utc(time, location): """ if isinstance(time, dt.datetime): if time.tzinfo is None: - time = pytz.timezone(location.tz).localize(time) + time = location.pytz.localize(time) time_utc = time.astimezone(pytz.utc) else: try: From 60a5d941343883fd051374ceca591b726d68eeb1 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 23 Dec 2024 14:57:18 -0700 Subject: [PATCH 13/42] Add timezone type checks to Location init test --- pvlib/location.py | 1 + pvlib/tests/test_location.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index e3ad7e6db1..34da92da63 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -72,6 +72,7 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=None, self.tz = 'UTC' self.pytz = pytz.UTC elif isinstance(tz, datetime.tzinfo): + # This includes pytz timezones. self.tz = tz.zone self.pytz = pytz.timezone(tz.zone) elif isinstance(tz, (int, float)): diff --git a/pvlib/tests/test_location.py b/pvlib/tests/test_location.py index e04b10ab4c..13fc6c9cc3 100644 --- a/pvlib/tests/test_location.py +++ b/pvlib/tests/test_location.py @@ -28,11 +28,16 @@ def test_location_all(): @pytest.mark.parametrize('tz', [ - pytz.timezone('US/Arizona'), 'America/Phoenix', -7, -7.0, - datetime.timezone.utc + 'America/Phoenix', + datetime.timezone.utc, + pytz.timezone('US/Arizona'), + -7, + -7.0, ]) def test_location_tz(tz): - Location(32.2, -111, tz) + loc = Location(32.2, -111, tz) + assert type(loc.tz) is str + assert isinstance(loc.pytz, pytz.tzinfo.BaseTzInfo) def test_location_invalid_tz(): From 9ab2ecfd85acccb492f383e3fb99fbfeff6f2f6f Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 23 Dec 2024 19:32:03 -0700 Subject: [PATCH 14/42] Don't parameterize repetitive tests --- pvlib/tests/test_tools.py | 290 ++++++++++++++------------------------ 1 file changed, 103 insertions(+), 187 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 1b5eb63c55..013716549b 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -149,190 +149,106 @@ def test_normalize_max2one(data_in, expected): assert_allclose(result, expected) -@pytest.mark.parametrize( - 'input,expected', - [ - ( - { - "time": datetime( - 1974, 6, 22, 18, 30, 15, tzinfo=ZoneInfo("Etc/GMT+5"), - ), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - datetime(1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC")), - ), - ( - { - "time": datetime(1974, 6, 22, 18, 30, 15), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - datetime(1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC")), - ), - ( - { - "time": pd.DatetimeIndex( - ["1974-06-22T18:30:15"], - tz=ZoneInfo("Etc/GMT+5"), - ), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), - ), - ( - { - "time": pd.DatetimeIndex(["1974-06-22T18:30:15"]), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), - ), - ( - { - "time": pd.Series( - [24.42], - index=pd.DatetimeIndex( - ["1974-06-22T18:30:15"], - tz=ZoneInfo("Etc/GMT+5"), - ), - ), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - pd.Series( - [24.42], - pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), - ), - ), - ( - { - "time": pd.Series( - [24.42], - index=pd.DatetimeIndex(["1974-06-22T18:30:15"]), - ), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - pd.Series( - [24.42], - pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), - ), - ), - ( - { - "time": pd.DataFrame( - [[24.42]], - index=pd.DatetimeIndex( - ["1974-06-22T18:30:15"], - tz=ZoneInfo("Etc/GMT+5"), - ), - ), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - pd.DataFrame( - [[24.42]], - pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), - ), - ), - ( - { - "time": pd.DataFrame( - [[24.42]], - index=pd.DatetimeIndex(["1974-06-22T18:30:15"]), - ), - "location": location.Location( - 43.19262774396091, -77.58782907414867, tz="Etc/GMT+5" - ) - }, - pd.DataFrame( - [[24.42]], - pd.DatetimeIndex(["1974-06-22T23:30:15"], tz=ZoneInfo("UTC")), - ), - ), - ], - ids=[ - "datetime.datetime with tzinfo", - "datetime.datetime", - "pandas.DatetimeIndex with tzinfo", - "pandas.DatetimeIndex", - "pandas.Series with tzinfo", - "pandas.Series", - "pandas.DataFrame with tzinfo", - "pandas.DataFrame", - ], -) -def test_localize_to_utc(input, expected): - got = tools.localize_to_utc(**input) - - if isinstance(got, (pd.Series, pd.DataFrame)): - # Older pandas versions have wonky dtype equality check on timestamp - # index, so check the values as numpy.ndarray and indices one by one. - np.testing.assert_array_equal(got.to_numpy(), expected.to_numpy()) - - for index_got, index_expected in zip(got.index, expected.index): - assert index_got == index_expected - else: - assert got == expected - - -@pytest.mark.parametrize( - 'input,expected', - [ - ( - { - "time": datetime( - 1974, 6, 22, 18, 30, 15, tzinfo=ZoneInfo("Etc/GMT+5") - ) - }, - 27201.47934027778, - ), - ( - { - "time": datetime(1974, 6, 22, 23, 30, 15) - }, - 27201.47934027778, - ), - ], - ids=["datetime.datetime with tzinfo", "datetime.datetime"], -) -def test_datetime_to_djd(input, expected): - assert tools.datetime_to_djd(input["time"]) == expected - - -@pytest.mark.parametrize( - 'input,expected', - [ - ( - { - "djd": 27201.47934027778, - "tz": "Etc/GMT+5", - }, - datetime(1974, 6, 22, 18, 30, 15, tzinfo=ZoneInfo("Etc/GMT+5")), - ), - ( - { - "djd": 27201.47934027778, - "tz": None, - }, - datetime(1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC")), - ), - ], - ids=["djd with tzinfo", "djd"], -) -def test_djd_to_datetime(input, expected): - if input["tz"] is not None: - got = tools.djd_to_datetime(input["djd"]) - else: - got = tools.djd_to_datetime(input["djd"], tz="Etc/GMT+5") - - assert got == expected +def test_localize_to_utc(): + lat, lon = 43.2, -77.6 + tz = "Etc/GMT+5" + loc = location.Location(lat, lon, tz=tz) + year, month, day, hour, minute, second = 1974, 6, 22, 18, 30, 15 + hour_utc = hour + 5 + + # Test all combinations of supported inputs. + dt_time_aware_utc = datetime( + year, month, day, hour_utc, minute, second, tzinfo=ZoneInfo("UTC") + ) + dt_time_aware = datetime( + year, month, day, hour, minute, second, tzinfo=ZoneInfo(tz) + ) + assert tools.localize_to_utc(dt_time_aware, None) == dt_time_aware_utc + dt_time_naive = datetime(year, month, day, hour, minute, second) + assert tools.localize_to_utc(dt_time_naive, loc) == dt_time_aware_utc + + # FIXME Derive timestamp strings from above variables. + dt_index_aware_utc = pd.DatetimeIndex( + [dt_time_aware_utc.strftime("%Y-%m-%dT%H:%M:%S")], tz=ZoneInfo("UTC") + ) + dt_index_aware = pd.DatetimeIndex( + [dt_time_aware.strftime("%Y-%m-%dT%H:%M:%S")], tz=ZoneInfo(tz) + ) + assert tools.localize_to_utc(dt_index_aware, None) == dt_index_aware_utc + dt_index_naive = pd.DatetimeIndex( + [dt_time_naive.strftime("%Y-%m-%dT%H:%M:%S")] + ) + assert tools.localize_to_utc(dt_index_naive, loc) == dt_index_aware_utc + + # Older pandas versions have wonky dtype equality check on timestamp + # index, so check the values as numpy.ndarray and indices one by one. + series_time_aware_utc_expected = pd.Series([24.42], dt_index_aware_utc) + series_time_aware = pd.Series([24.42], index=dt_index_aware) + series_time_aware_utc_got = tools.localize_to_utc(series_time_aware, None) + np.testing.assert_array_equal( + series_time_aware_utc_got.to_numpy(), + series_time_aware_utc_expected.to_numpy(), + ) + + for index_got, index_expected in zip( + series_time_aware_utc_got.index, series_time_aware_utc_expected.index + ): + assert index_got == index_expected + + series_time_naive = pd.Series([24.42], index=dt_index_naive) + series_time_naive_utc_got = tools.localize_to_utc(series_time_naive, loc) + np.testing.assert_array_equal( + series_time_naive_utc_got.to_numpy(), + series_time_aware_utc_expected.to_numpy(), + ) + + for index_got, index_expected in zip( + series_time_naive_utc_got.index, series_time_aware_utc_expected.index + ): + assert index_got == index_expected + + # Older pandas versions have wonky dtype equality check on timestamp + # index, so check the values as numpy.ndarray and indices one by one. + df_time_aware_utc_expected = pd.DataFrame([[24.42]], dt_index_aware) + df_time_naive = pd.DataFrame([[24.42]], index=dt_index_naive) + df_time_naive_utc_got = tools.localize_to_utc(df_time_naive, loc) + np.testing.assert_array_equal( + df_time_naive_utc_got.to_numpy(), + df_time_aware_utc_expected.to_numpy(), + ) + + for index_got, index_expected in zip( + df_time_naive_utc_got.index, df_time_aware_utc_expected.index + ): + assert index_got == index_expected + + df_time_aware = pd.DataFrame([[24.42]], index=dt_index_aware) + df_time_aware_utc_got = tools.localize_to_utc(df_time_aware, None) + np.testing.assert_array_equal( + df_time_aware_utc_got.to_numpy(), + df_time_aware_utc_expected.to_numpy(), + ) + + for index_got, index_expected in zip( + df_time_aware_utc_got.index, df_time_aware_utc_expected.index + ): + assert index_got == index_expected + + +def test_datetime_to_djd(): + expected = 27201.47934027778 + dt_aware = datetime(1974, 6, 22, 18, 30, 15, tzinfo=ZoneInfo("Etc/GMT+5")) + assert tools.datetime_to_djd(dt_aware) == expected + dt_naive_utc = datetime(1974, 6, 22, 23, 30, 15) + assert tools.datetime_to_djd(dt_naive_utc) == expected + + +def test_djd_to_datetime(): + djd = 27201.47934027778 + tz = "Etc/GMT+5" + + expected = datetime(1974, 6, 22, 18, 30, 15, tzinfo=ZoneInfo(tz)) + assert tools.djd_to_datetime(djd, tz) == expected + + expected = datetime(1974, 6, 22, 23, 30, 15, tzinfo=ZoneInfo("UTC")) + assert tools.djd_to_datetime(djd) == expected From ddef8d11b0b1a6aebdb649e8e64db8aff9452923 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 23 Dec 2024 19:36:30 -0700 Subject: [PATCH 15/42] Update whatsnew for Location bugfix --- docs/sphinx/source/whatsnew/v0.11.3.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.3.rst b/docs/sphinx/source/whatsnew/v0.11.3.rst index 82ccaa2381..8a911927e9 100644 --- a/docs/sphinx/source/whatsnew/v0.11.3.rst +++ b/docs/sphinx/source/whatsnew/v0.11.3.rst @@ -12,13 +12,21 @@ Enhancements ~~~~~~~~~~~~ +Bug Fixes +~~~~~~~~~ +* Ensure proper tz and pytz types in pvlib.location.Location. (:issue:`2340`, +:pull:`2341`) + + Documentation ~~~~~~~~~~~~~ Testing ~~~~~~~ -* Add tests for time conversions in tools package. (:issue:`2340`, :pull:`2341`) +* Add tests for timezone types in pvlib.location.Location. (:issue:`2340`, +:pull:`2341`) +* Add tests for time conversions in pvlib.tools. (:issue:`2340`, :pull:`2341`) Requirements From 4f17f490cad31607649faafa4263da8ca986864a Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 23 Dec 2024 19:48:45 -0700 Subject: [PATCH 16/42] Update docstring --- pvlib/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tools.py b/pvlib/tools.py index 535b197a25..305acb5b85 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -125,7 +125,7 @@ def localize_to_utc(time, location): ---------- time : datetime.datetime, pandas.DatetimeIndex, or pandas.Series/DataFrame with a DatetimeIndex. - location : pvlib.Location object + location : pvlib.Location object (unused if time is localized) Returns ------- From a3c3e03c4b123636d55fd55c393ab8eea9fd34cd Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 23 Dec 2024 19:51:49 -0700 Subject: [PATCH 17/42] Improve whatsnew formatting --- docs/sphinx/source/whatsnew/v0.11.3.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.3.rst b/docs/sphinx/source/whatsnew/v0.11.3.rst index 8a911927e9..0299558f0b 100644 --- a/docs/sphinx/source/whatsnew/v0.11.3.rst +++ b/docs/sphinx/source/whatsnew/v0.11.3.rst @@ -14,8 +14,8 @@ Enhancements Bug Fixes ~~~~~~~~~ -* Ensure proper tz and pytz types in pvlib.location.Location. (:issue:`2340`, -:pull:`2341`) +* Ensure proper tz and pytz types in pvlib.location.Location. + (:issue:`2340`, :pull:`2341`) Documentation @@ -24,8 +24,8 @@ Documentation Testing ~~~~~~~ -* Add tests for timezone types in pvlib.location.Location. (:issue:`2340`, -:pull:`2341`) +* Add tests for timezone types in pvlib.location.Location. + (:issue:`2340`, :pull:`2341`) * Add tests for time conversions in pvlib.tools. (:issue:`2340`, :pull:`2341`) From 5f59417cc6f9160482941e4af0dd33ab58df9d72 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Thu, 9 Jan 2025 11:22:35 -0700 Subject: [PATCH 18/42] Support non-fractional int and float and pytz and zoneinfo time zones --- pvlib/location.py | 81 ++++++++++++++++++++++-------------- pvlib/tests/test_location.py | 69 ++++++++++++++++++++++++------ 2 files changed, 106 insertions(+), 44 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index 34da92da63..730f901606 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -18,13 +18,15 @@ class Location: """ Location objects are convenient containers for latitude, longitude, - timezone, and altitude data associated with a particular - geographic location. You can also assign a name to a location object. + 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 timezone attributes: + 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: - * ``tz`` is a IANA timezone string. - * ``pytz`` is a pytz timezone object. + * ``tz`` is a IANA time-zone string. + * ``pytz`` is a pytz time-zone object. Location objects support the print method. @@ -38,12 +40,16 @@ class Location: Positive is east of the prime meridian. Use decimal degrees notation. - tz : str, int, float, or pytz.timezone, default 'UTC'. - See - http://en.wikipedia.org/wiki/List_of_tz_database_time_zones - for a list of valid time zones. - pytz.timezone objects will be converted to strings. - ints and floats must be in hours from UTC. + 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). + Raises TypeError for time zone conversion issues or + pytz.exceptions.UnknownTimeZoneError when (stringified) time zone is + not recognized by pytz.timezone. altitude : float, optional Altitude from sea level in meters. @@ -59,33 +65,17 @@ class Location: pvlib.pvsystem.PVSystem """ - def __init__(self, latitude, longitude, tz='UTC', altitude=None, - name=None): - + def __init__( + self, latitude, longitude, tz='UTC', altitude=None, name=None + ): self.latitude = latitude self.longitude = longitude - - if isinstance(tz, str): - self.tz = tz - self.pytz = pytz.timezone(tz) - elif isinstance(tz, datetime.timezone): - self.tz = 'UTC' - self.pytz = pytz.UTC - elif isinstance(tz, datetime.tzinfo): - # This includes pytz timezones. - self.tz = tz.zone - self.pytz = pytz.timezone(tz.zone) - elif isinstance(tz, (int, float)): - self.tz = f"Etc/GMT{int(-tz):+d}" - self.pytz = pytz.timezone(self.tz) - else: - raise TypeError('Invalid tz specification') + self.tz = tz if altitude is None: altitude = lookup_altitude(latitude, longitude) self.altitude = altitude - self.name = name def __repr__(self): @@ -95,6 +85,35 @@ def __repr__(self): return ('Location: \n ' + '\n '.join( f'{attr}: {getattr(self, attr, None)}' for attr in attrs)) + @property + def tz(self): + # self.pytz holds the single source of time-zone truth. + return self.pytz.zone + + @tz.setter + def tz(self, tz_): + if isinstance(tz_, str): + self.pytz = pytz.timezone(tz_) + elif isinstance(tz_, int): + self.pytz = pytz.timezone(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_}" + ) + + self.pytz = pytz.timezone(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_)) + else: + raise TypeError( + f"invalid tz specification: {tz_}, must be an IANA time zone " + "string, a non-fractional int/float UTC offset, or a " + "datetime.tzinfo object (including subclasses)" + ) + @classmethod def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs): """ diff --git a/pvlib/tests/test_location.py b/pvlib/tests/test_location.py index 13fc6c9cc3..6371dbcb5e 100644 --- a/pvlib/tests/test_location.py +++ b/pvlib/tests/test_location.py @@ -1,5 +1,6 @@ import datetime from unittest.mock import ANY +import zoneinfo import numpy as np from numpy import nan @@ -27,27 +28,69 @@ def test_location_all(): Location(32.2, -111, 'US/Arizona', 700, 'Tucson') -@pytest.mark.parametrize('tz', [ - 'America/Phoenix', - datetime.timezone.utc, - pytz.timezone('US/Arizona'), - -7, - -7.0, -]) -def test_location_tz(tz): +@pytest.mark.parametrize( + 'tz,tz_expected', + [ + pytest.param('UTC', 'UTC'), + pytest.param('Etc/GMT+5', 'Etc/GMT+5'), + pytest.param('US/Mountain','US/Mountain'), + pytest.param('America/Phoenix', 'America/Phoenix'), + pytest.param('Asia/Kathmandu', 'Asia/Kathmandu'), + 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(-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 type(loc.tz) is str + assert isinstance(loc.pytz, datetime.tzinfo) # Abstract base class. assert isinstance(loc.pytz, pytz.tzinfo.BaseTzInfo) + 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') + + # Updating tz updates pytz. + loc.tz = 7 + 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') -def test_location_invalid_tz(): +@pytest.mark.parametrize( + 'tz', [ + 'invalid', + 'Etc/GMT+20', # offset too large. + 20, # offset too large. + ] +) +def test_location_invalid_tz(tz): with pytest.raises(UnknownTimeZoneError): - Location(32.2, -111, 'invalid') + Location(32.2, -111, tz) -def test_location_invalid_tz_type(): +@pytest.mark.parametrize( + 'tz', [ + -9.5, # float with non-zero fractional part. + b"bytes not str", + [5], + ] +) +def test_location_invalid_tz_type(tz): with pytest.raises(TypeError): - Location(32.2, -111, [5]) + Location(32.2, -111, tz) def test_location_print_all(): From c84801f3cc4f795dd959b8dfe205cffd68a1256a Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Thu, 9 Jan 2025 11:24:40 -0700 Subject: [PATCH 19/42] Appease the linter --- pvlib/location.py | 2 +- pvlib/tests/test_location.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index 730f901606..d97ba59a8b 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -47,7 +47,7 @@ class Location: 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). - Raises TypeError for time zone conversion issues or + Raises TypeError for time zone conversion issues or pytz.exceptions.UnknownTimeZoneError when (stringified) time zone is not recognized by pytz.timezone. diff --git a/pvlib/tests/test_location.py b/pvlib/tests/test_location.py index 6371dbcb5e..ad26344559 100644 --- a/pvlib/tests/test_location.py +++ b/pvlib/tests/test_location.py @@ -29,11 +29,10 @@ def test_location_all(): @pytest.mark.parametrize( - 'tz,tz_expected', - [ + 'tz,tz_expected', [ pytest.param('UTC', 'UTC'), pytest.param('Etc/GMT+5', 'Etc/GMT+5'), - pytest.param('US/Mountain','US/Mountain'), + pytest.param('US/Mountain', 'US/Mountain'), pytest.param('America/Phoenix', 'America/Phoenix'), pytest.param('Asia/Kathmandu', 'Asia/Kathmandu'), pytest.param('Asia/Yangon', 'Asia/Yangon'), From 195efbc5b506d14bc6a7dd7184d6cb22dc7dd8a7 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 10 Jan 2025 08:32:22 -0700 Subject: [PATCH 20/42] Use zoneinfo as single source of truth and tz as interface point --- pvlib/location.py | 56 ++++++++++++++++++++++-------------- pvlib/tests/test_location.py | 8 +----- pvlib/tools.py | 5 ++-- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index d97ba59a8b..50527c0209 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -6,6 +6,7 @@ import pathlib import datetime +import zoneinfo import pandas as pd import pytz @@ -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. @@ -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. @@ -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): """ diff --git a/pvlib/tests/test_location.py b/pvlib/tests/test_location.py index ad26344559..906e6b263b 100644 --- a/pvlib/tests/test_location.py +++ b/pvlib/tests/test_location.py @@ -10,7 +10,6 @@ import pytest import pytz -from pytz.exceptions import UnknownTimeZoneError import pvlib from pvlib import location @@ -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', [ @@ -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) diff --git a/pvlib/tools.py b/pvlib/tools.py index 305acb5b85..80b3d4120b 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -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): @@ -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 ------- From 1a5efed1fe80d043512da9ef3e6b6efdbdc34307 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 10 Jan 2025 08:50:40 -0700 Subject: [PATCH 21/42] Add zoneinfo asserts in tests --- pvlib/tests/test_location.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_location.py b/pvlib/tests/test_location.py index 906e6b263b..08db9e7a10 100644 --- a/pvlib/tests/test_location.py +++ b/pvlib/tests/test_location.py @@ -54,12 +54,14 @@ def test_location_tz(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') + assert loc.pytz == pytz.timezone('Etc/GMT+11') # Deprecated attribute. + assert loc.zoneinfo == zoneinfo.ZoneInfo('Etc/GMT+11') - # Updating tz updates pytz. + # 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') + assert loc.pytz == pytz.timezone('Etc/GMT-7') # Deprecated attribute. + assert loc.zoneinfo == zoneinfo.ZoneInfo('Etc/GMT-7') @pytest.mark.parametrize( From e5af9aeb5b426b7c8750ecfec205fb053e4f3fe4 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 10 Jan 2025 09:19:29 -0700 Subject: [PATCH 22/42] Try to fix asv ci --- .github/workflows/asv_check.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/asv_check.yml b/.github/workflows/asv_check.yml index e72896be08..8e01f5478e 100644 --- a/.github/workflows/asv_check.yml +++ b/.github/workflows/asv_check.yml @@ -39,6 +39,7 @@ jobs: uses: conda-incubator/setup-miniconda@v3 with: conda-version: 24.1.2 + python-version: 3.9 - name: Run asv benchmarks run: | From 67e98442c56f5cff3f29a2f17e677b8b46dca8f9 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 10 Jan 2025 09:27:30 -0700 Subject: [PATCH 23/42] See if newer asv works with newer conda --- .github/workflows/asv_check.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/asv_check.yml b/.github/workflows/asv_check.yml index 8e01f5478e..7c185b3d1e 100644 --- a/.github/workflows/asv_check.yml +++ b/.github/workflows/asv_check.yml @@ -26,7 +26,7 @@ jobs: python-version: '3.9' - name: Install asv - run: pip install asv==0.4.2 + run: pip install asv==0.6.4 # asv 0.4.2 (and more recent versions as well) creates conda envs # using the --force option, which was removed in conda 24.3. @@ -35,11 +35,11 @@ jobs: # To prevent that, we install an older version. # TODO: remove this when we eventually upgrade our asv version. # https://github.com/airspeed-velocity/asv/issues/1396 - - name: Install Conda - uses: conda-incubator/setup-miniconda@v3 - with: - conda-version: 24.1.2 - python-version: 3.9 + # - name: Install Conda + # uses: conda-incubator/setup-miniconda@v3 + # with: + # # conda-version: 24.1.2 + # python-version: 3.9 - name: Run asv benchmarks run: | From e35eb420c58348305ee20fa3fa037f9f6cc69e54 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 10 Jan 2025 09:33:17 -0700 Subject: [PATCH 24/42] Remove comments no longer needed --- .github/workflows/asv_check.yml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.github/workflows/asv_check.yml b/.github/workflows/asv_check.yml index 7c185b3d1e..3dc7646c28 100644 --- a/.github/workflows/asv_check.yml +++ b/.github/workflows/asv_check.yml @@ -28,19 +28,6 @@ jobs: - name: Install asv run: pip install asv==0.6.4 - # asv 0.4.2 (and more recent versions as well) creates conda envs - # using the --force option, which was removed in conda 24.3. - # Since ubuntu-latest now comes with conda 24.3 pre-installed, - # using the system's conda will result in error. - # To prevent that, we install an older version. - # TODO: remove this when we eventually upgrade our asv version. - # https://github.com/airspeed-velocity/asv/issues/1396 - # - name: Install Conda - # uses: conda-incubator/setup-miniconda@v3 - # with: - # # conda-version: 24.1.2 - # python-version: 3.9 - - name: Run asv benchmarks run: | cd benchmarks From a1a0261743988ee5f5a8738f809eb314333ed151 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 10 Jan 2025 10:30:22 -0700 Subject: [PATCH 25/42] Remove addition of zoneinfo attribute --- pvlib/location.py | 26 ++++++++++++++------------ pvlib/tests/test_location.py | 2 -- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index 50527c0209..5c7dc62cd9 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -26,11 +26,10 @@ class Location: * ``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``. + As with Location-object initialization, use the ``tz`` attribute to update + the Location's object's time zone after instantiation, and the read-only + ``pytz`` attribute will stay in sync with any changes made using ``tz``. Location objects support the print method. @@ -52,10 +51,6 @@ class Location: 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 - 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. @@ -66,6 +61,17 @@ class Location: name : string, optional Sets the name attribute of the Location object. + + Raises + ------ + ValueError + when the time-zone ``tz`` input cannot be converted. + + zoneinfo.ZoneInfoNotFoundError + when the time zone ``tz`` is not recognizable as an IANA time zone by + the zoneinfo.ZoneInfo initializer used for internal time-zone + representation. + See also -------- pvlib.pvsystem.PVSystem @@ -124,10 +130,6 @@ def tz(self, tz_): 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): """ diff --git a/pvlib/tests/test_location.py b/pvlib/tests/test_location.py index 08db9e7a10..6d06f0f4a7 100644 --- a/pvlib/tests/test_location.py +++ b/pvlib/tests/test_location.py @@ -55,13 +55,11 @@ 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. - assert loc.zoneinfo == zoneinfo.ZoneInfo('Etc/GMT+11') # 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. - assert loc.zoneinfo == zoneinfo.ZoneInfo('Etc/GMT-7') @pytest.mark.parametrize( From 8373ac4a8b9b2e820bb9cdac7bb8f6d5cf38f920 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 10 Jan 2025 10:38:57 -0700 Subject: [PATCH 26/42] Revise whatsnew bugfix --- docs/sphinx/source/whatsnew/v0.11.3.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.3.rst b/docs/sphinx/source/whatsnew/v0.11.3.rst index 0299558f0b..2f4396f47f 100644 --- a/docs/sphinx/source/whatsnew/v0.11.3.rst +++ b/docs/sphinx/source/whatsnew/v0.11.3.rst @@ -14,7 +14,10 @@ Enhancements Bug Fixes ~~~~~~~~~ -* Ensure proper tz and pytz types in pvlib.location.Location. +* Ensure proper tz and pytz types in pvlib.location.Location. tz becomes the + single source of time-zone truth, is the single time-zone setter interface, + and the getter consistently returns an IANA string. pytz attribute becomes + read only. Any datetime.tzinfo subclass is fully supported in tz setter. (:issue:`2340`, :pull:`2341`) From eee6f51563deff108fbe2dbf40ef2c52df4df6a4 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 10 Jan 2025 10:39:10 -0700 Subject: [PATCH 27/42] Revise whatsnew bugfix more --- docs/sphinx/source/whatsnew/v0.11.3.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.3.rst b/docs/sphinx/source/whatsnew/v0.11.3.rst index 2f4396f47f..d64dc96fe5 100644 --- a/docs/sphinx/source/whatsnew/v0.11.3.rst +++ b/docs/sphinx/source/whatsnew/v0.11.3.rst @@ -16,9 +16,9 @@ Bug Fixes ~~~~~~~~~ * Ensure proper tz and pytz types in pvlib.location.Location. tz becomes the single source of time-zone truth, is the single time-zone setter interface, - and the getter consistently returns an IANA string. pytz attribute becomes - read only. Any datetime.tzinfo subclass is fully supported in tz setter. - (:issue:`2340`, :pull:`2341`) + and the getter consistently returns an IANA string. datetime.tzinfo + subclasses are fully supported in tz setter. pytz attribute becomes read + only. (:issue:`2340`, :pull:`2341`) Documentation From 9662c1f91158a2e62eb612cf0ea95418b7d44dc3 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 10 Jan 2025 10:40:07 -0700 Subject: [PATCH 28/42] Spell my name correctly --- docs/sphinx/source/whatsnew/v0.11.3.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.3.rst b/docs/sphinx/source/whatsnew/v0.11.3.rst index d64dc96fe5..9b4db20dd7 100644 --- a/docs/sphinx/source/whatsnew/v0.11.3.rst +++ b/docs/sphinx/source/whatsnew/v0.11.3.rst @@ -38,4 +38,4 @@ Requirements Contributors ~~~~~~~~~~~~ -* Mark Campanellli (:ghuser:`markcampanelli`) +* Mark Campanelli (:ghuser:`markcampanelli`) From 32284ba5b7f98ce048fe3149a4a41728ab03f63e Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 10 Jan 2025 10:41:24 -0700 Subject: [PATCH 29/42] The linter strikes back again --- pvlib/location.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pvlib/location.py b/pvlib/location.py index 5c7dc62cd9..716a27e2f3 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -61,7 +61,6 @@ class Location: name : string, optional Sets the name attribute of the Location object. - Raises ------ ValueError From c09a32862f44fb32559dedfc14839f08d798060f Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 27 Jan 2025 07:37:36 -0700 Subject: [PATCH 30/42] Fix whatsnew after main merge --- docs/sphinx/source/whatsnew/v0.11.3.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.3.rst b/docs/sphinx/source/whatsnew/v0.11.3.rst index 8629756c0a..fce6443939 100644 --- a/docs/sphinx/source/whatsnew/v0.11.3.rst +++ b/docs/sphinx/source/whatsnew/v0.11.3.rst @@ -36,8 +36,6 @@ Requirements ~~~~~~~~~~~~ -Contributors -~~~~~~~~~~~~ Maintenance ~~~~~~~~~~~ * Fix ReadTheDocs builds by upgrading `readthedocs.yml` configuration From 4ef4b69c28c8c1204cad5f27762ce29df5f33b3b Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 27 Jan 2025 17:46:33 -0700 Subject: [PATCH 31/42] Address Cliff's comment --- pvlib/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tools.py b/pvlib/tools.py index 80b3d4120b..b08d061676 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -120,7 +120,7 @@ def atand(number): def localize_to_utc(time, location): """ - Converts time to UTC, localizing if necessary using location. + Converts ``time`` to UTC, localizing if necessary using location. Parameters ---------- From 7490792cea9784b4e27b212a64af29e94aebfc67 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 27 Jan 2025 18:16:18 -0700 Subject: [PATCH 32/42] Adjust Location documentation --- pvlib/location.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index 716a27e2f3..efe8a92b9c 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -44,13 +44,13 @@ class Location: Use decimal degrees notation. tz : time zone as str, int, float, or datetime.tzinfo (inc. subclasses - from the pytz and zoneinfo packages), default 'UTC'. + from the pytz and zoneinfo packages), default 'UTC'. 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). + `int`s and `float`s 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.) altitude : float, optional Altitude from sea level in meters. @@ -68,7 +68,7 @@ class Location: zoneinfo.ZoneInfoNotFoundError when the time zone ``tz`` is not recognizable as an IANA time zone by - the zoneinfo.ZoneInfo initializer used for internal time-zone + the `zoneinfo.ZoneInfo` initializer used for internal time-zone representation. See also From a5f7646a10421e1eb3be331f151b38ebb1b99902 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 27 Jan 2025 18:25:11 -0700 Subject: [PATCH 33/42] Fix indent --- pvlib/location.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index efe8a92b9c..7a3958a5f3 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -44,7 +44,7 @@ class Location: Use decimal degrees notation. tz : time zone as str, int, float, or datetime.tzinfo (inc. subclasses - from the pytz and zoneinfo packages), default 'UTC'. + from the pytz and zoneinfo packages), default 'UTC'. 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. @@ -68,7 +68,7 @@ class Location: zoneinfo.ZoneInfoNotFoundError when the time zone ``tz`` is not recognizable as an IANA time zone by - the `zoneinfo.ZoneInfo` initializer used for internal time-zone + the ``zoneinfo.ZoneInfo`` initializer used for internal time-zone representation. See also From 1382e30a4edcf87d3466f46f79c36b47cca9a2ed Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 27 Jan 2025 18:33:23 -0700 Subject: [PATCH 34/42] More docstring tweaks --- pvlib/location.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index 7a3958a5f3..a6e9531ce2 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -43,12 +43,12 @@ class Location: Positive is east of the prime meridian. Use decimal degrees notation. - tz : time zone as str, int, float, or datetime.tzinfo (inc. subclasses - from the pytz and zoneinfo packages), default 'UTC'. + tz : time zone as str, int, float, or datetime.tzinfo (including + subclassesfrom the pytz and zoneinfo packages), default 'UTC'. 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. - `int`s and `float`s must be whole-number hour offsets from UTC, which + 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.) @@ -98,6 +98,7 @@ def __repr__(self): @property def tz(self): + """The location's IANA time-zone string.""" return str(self._zoneinfo) @tz.setter @@ -127,6 +128,7 @@ def tz(self, tz_): @property def pytz(self): + """The location's pytz time zone.""" return pytz.timezone(str(self._zoneinfo)) @classmethod From 059e35f57d23a1ba9c861636210b2f0fa1d80bac Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 27 Jan 2025 18:42:41 -0700 Subject: [PATCH 35/42] Try to fix bad parens --- pvlib/location.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index a6e9531ce2..f6db5a8603 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -43,8 +43,8 @@ class Location: Positive is east of the prime meridian. Use decimal degrees notation. - tz : time zone as str, int, float, or datetime.tzinfo (including - subclassesfrom the pytz and zoneinfo packages), default 'UTC'. + tz : time zone as str, int, float, or datetime.tzinfo including + subclassesfrom the pytz and zoneinfo packages, default 'UTC'. 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. From f9f07d7d382f2c0d4192fededb82c7e75bf88cfa Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 27 Jan 2025 18:51:39 -0700 Subject: [PATCH 36/42] Rearrange docstring --- pvlib/location.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index f6db5a8603..71a093fcf4 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -43,14 +43,15 @@ class Location: Positive is east of the prime meridian. Use decimal degrees notation. - tz : time zone as str, int, float, or datetime.tzinfo including - subclassesfrom the pytz and zoneinfo packages, default 'UTC'. - This value is stored as a valid IANA time zone name string. See + tz : time zone as str, int, float, or datetime.tzinfo, default 'UTC'. + This value represents 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.) + limited range of the offset N and its sign-change convention.) + Time zones from the pytz and zoneinfo packages may also be passed + directly here, as they are subclasses of datetime.tzinfo. altitude : float, optional Altitude from sea level in meters. From 75db2aade701d52d38ad0c28fc18e87384b1c31b Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 27 Jan 2025 18:52:58 -0700 Subject: [PATCH 37/42] Appease the linter --- pvlib/location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/location.py b/pvlib/location.py index 71a093fcf4..025594b09d 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -49,7 +49,7 @@ class Location: 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.) + limited range of the offset N and its sign-change convention.) Time zones from the pytz and zoneinfo packages may also be passed directly here, as they are subclasses of datetime.tzinfo. From 1164c965294d98744831c6f96d37143f6312a3b2 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 27 Jan 2025 18:59:15 -0700 Subject: [PATCH 38/42] Document pytz attribute as read only --- pvlib/location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/location.py b/pvlib/location.py index 025594b09d..ab7f4df8cb 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -129,7 +129,7 @@ def tz(self, tz_): @property def pytz(self): - """The location's pytz time zone.""" + """The location's pytz time zone (read only).""" return pytz.timezone(str(self._zoneinfo)) @classmethod From 5f6ad145ff848e6bea49c9a8c4b10aae19b9d58a Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Mon, 27 Jan 2025 19:04:42 -0700 Subject: [PATCH 39/42] Consistent read only --- pvlib/location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/location.py b/pvlib/location.py index ab7f4df8cb..9932fb6363 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -25,7 +25,7 @@ class Location: Location objects have two time-zone attributes: * ``tz`` is an IANA time-zone string. - * ``pytz`` is a pytz-based time-zone object (read-only). + * ``pytz`` is a pytz-based time-zone object (read only). As with Location-object initialization, use the ``tz`` attribute to update the Location's object's time zone after instantiation, and the read-only From f691bb6a08c1eed23473ea29b3c0b8647fbf2429 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Tue, 28 Jan 2025 09:57:04 -0700 Subject: [PATCH 40/42] Update pvlib/location.py per review comment Co-authored-by: Cliff Hansen --- pvlib/location.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pvlib/location.py b/pvlib/location.py index 9932fb6363..5559fb6a28 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -27,9 +27,7 @@ class Location: * ``tz`` is an IANA time-zone string. * ``pytz`` is a pytz-based time-zone object (read only). - As with Location-object initialization, use the ``tz`` attribute to update - the Location's object's time zone after instantiation, and the read-only - ``pytz`` attribute will stay in sync with any changes made using ``tz``. + The read-only ``pytz`` attribute will stay in sync with any changes made using ``tz``. Location objects support the print method. From 7cfb170b297b2d87f930f07e2abf6a2a286a95e3 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Tue, 28 Jan 2025 10:08:12 -0700 Subject: [PATCH 41/42] Add breaking change to whatsnew and fix linting --- docs/sphinx/source/whatsnew/v0.11.3.rst | 16 +++++++++++----- pvlib/location.py | 3 ++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.3.rst b/docs/sphinx/source/whatsnew/v0.11.3.rst index fce6443939..5496a29eb0 100644 --- a/docs/sphinx/source/whatsnew/v0.11.3.rst +++ b/docs/sphinx/source/whatsnew/v0.11.3.rst @@ -14,11 +14,8 @@ Enhancements Bug Fixes ~~~~~~~~~ -* Ensure proper tz and pytz types in pvlib.location.Location. tz becomes the - single source of time-zone truth, is the single time-zone setter interface, - and the getter consistently returns an IANA string. datetime.tzinfo - subclasses are fully supported in tz setter. pytz attribute becomes read - only. (:issue:`2340`, :pull:`2341`) +* Ensure proper tz and pytz types in pvlib.location.Location. + (:issue:`2340`, :pull:`2341`) Documentation @@ -43,6 +40,15 @@ Maintenance * asv 0.4.2 upgraded to asv 0.6.4 to fix CI failure due to pinned older conda. (:pull:`2352`) +Breaking Changes +~~~~~~~~~~~~~~~~ +* To ensure that the time zone in pvlib.location.Location remains internally + consistent if/when the time zone is updated, the tz attribute becomes the + single source of time-zone truth, is the single time-zone setter interface, + and its getter consistently returns an IANA string. datetime.tzinfo + subclasses (including time zones from pytz and zoneinfo) are still fully + supported in the tz setter, but the pytz attribute becomes read only. + (:issue:`2340`, :pull:`2341`) Contributors ~~~~~~~~~~~~ diff --git a/pvlib/location.py b/pvlib/location.py index 5559fb6a28..8adc7acaa6 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -27,7 +27,8 @@ class Location: * ``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 read-only ``pytz`` attribute will stay in sync with any changes made + using ``tz``. Location objects support the print method. From ef5c60fad3f43817c7bd842938f0d9d1c1c5aeeb Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Wed, 5 Feb 2025 12:58:30 -0700 Subject: [PATCH 42/42] Clarify breaking change in whatsnew --- docs/sphinx/source/whatsnew/v0.11.3.rst | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.11.3.rst b/docs/sphinx/source/whatsnew/v0.11.3.rst index 5496a29eb0..a8eddd7518 100644 --- a/docs/sphinx/source/whatsnew/v0.11.3.rst +++ b/docs/sphinx/source/whatsnew/v0.11.3.rst @@ -14,8 +14,11 @@ Enhancements Bug Fixes ~~~~~~~~~ -* Ensure proper tz and pytz types in pvlib.location.Location. - (:issue:`2340`, :pull:`2341`) +* Ensure proper tz and pytz types in pvlib.location.Location. To ensure that + the time zone in pvlib.location.Location remains internally consistent + if/when the time zone is updated, the tz attribute is now the single source + of time-zone truth, is the single time-zone setter interface, and its getter + returns an IANA string. (:issue:`2340`, :pull:`2341`) Documentation @@ -42,13 +45,9 @@ Maintenance Breaking Changes ~~~~~~~~~~~~~~~~ -* To ensure that the time zone in pvlib.location.Location remains internally - consistent if/when the time zone is updated, the tz attribute becomes the - single source of time-zone truth, is the single time-zone setter interface, - and its getter consistently returns an IANA string. datetime.tzinfo - subclasses (including time zones from pytz and zoneinfo) are still fully - supported in the tz setter, but the pytz attribute becomes read only. - (:issue:`2340`, :pull:`2341`) +* The pytz attribute in pvlib.location.Location is now read only. pytz time + zones can still be passed to tz in the object initializer and in the tz + setter after object creation. (:issue:`2340`, :pull:`2341`) Contributors ~~~~~~~~~~~~