diff --git a/docs/sphinx/source/whatsnew/v0.7.2.rst b/docs/sphinx/source/whatsnew/v0.7.2.rst index 35511e6202..367a8ab1c6 100644 --- a/docs/sphinx/source/whatsnew/v0.7.2.rst +++ b/docs/sphinx/source/whatsnew/v0.7.2.rst @@ -3,6 +3,11 @@ v0.7.2 (Month day, year) ------------------------- +API Changes +~~~~~~~~~~~ +* :py:class:`pvlib.forecast.ForecastModel` now requires ``start`` and ``end`` + arguments to be tz-localized. (:issue:`877`, :pull:`879`) + Enhancements ~~~~~~~~~~~~ * TMY3 dataframe returned by :py:func:`~pvlib.iotools.read_tmy3` now contains @@ -15,6 +20,8 @@ Bug fixes a leap year (:pull:`866`) * Implement NREL Developer Network API key for consistent success with API calls in :py:mod:`pvlib.tests.iotools.test_psm3` (:pull:`873`) +* Fix issue with :py:class:`pvlib.location.Location` creation when + passing ``tz=datetime.timezone.utc`` (:pull:`879`) Documentation ~~~~~~~~~~~~~ @@ -29,3 +36,6 @@ Contributors * Mark Mikofski (:ghuser:`mikofski`) * Cliff Hansen (:ghuser:`cwhanse`) * Cameron T. Stark (:ghuser:`camerontstark`) +* Will Holmgren (:ghuser:`wholmgren`) +* Kevin Anderson (:ghuser:`kanderso-nrel`) +* Karthikeyan Singaravelan (:ghuser:`tirkarthi`) diff --git a/pvlib/forecast.py b/pvlib/forecast.py index cbea9bb8c8..9533b9535b 100644 --- a/pvlib/forecast.py +++ b/pvlib/forecast.py @@ -165,6 +165,25 @@ def set_dataset(self): self.ncss = NCSS(self.access_url) self.query = self.ncss.query() + def set_query_time_range(self, start, end): + """ + Parameters + ---------- + start : datetime.datetime, pandas.Timestamp + Must be tz-localized. + end : datetime.datetime, pandas.Timestamp + Must be tz-localized. + + Notes + ----- + Assigns ``self.start``, ``self.end``. Modifies ``self.query`` + """ + self.start = pd.Timestamp(start) + self.end = pd.Timestamp(end) + if self.start.tz is None or self.end.tz is None: + raise TypeError('start and end must be tz-localized') + self.query.time_range(self.start, self.end) + def set_query_latlon(self): ''' Sets the NCSS query location latitude and longitude. @@ -180,24 +199,24 @@ def set_query_latlon(self): self.lbox = False self.query.lonlat_point(self.longitude, self.latitude) - def set_location(self, time, latitude, longitude): + def set_location(self, tz, latitude, longitude): ''' Sets the location for the query. Parameters ---------- - time: datetime or DatetimeIndex - Time range of the query. - ''' - if isinstance(time, datetime.datetime): - tzinfo = time.tzinfo - else: - tzinfo = time.tz + tz: tzinfo + Timezone of the query + latitude: float + Latitude of the query + longitude: float + Longitude of the query - if tzinfo is None: - self.location = Location(latitude, longitude) - else: - self.location = Location(latitude, longitude, tz=tzinfo) + Notes + ----- + Assigns ``self.location``. + ''' + self.location = Location(latitude, longitude, tz=tz) def get_data(self, latitude, longitude, start, end, vert_level=None, query_variables=None, @@ -243,14 +262,12 @@ def get_data(self, latitude, longitude, start, end, else: self.query_variables = query_variables + self.set_query_time_range(start, end) + self.latitude = latitude self.longitude = longitude self.set_query_latlon() # modifies self.query - self.set_location(start, latitude, longitude) - - self.start = start - self.end = end - self.query.time_range(self.start, self.end) + self.set_location(self.start.tz, latitude, longitude) if self.vert_level is not None: self.query.vertical_level(self.vert_level) diff --git a/pvlib/iotools/ecmwf_macc.py b/pvlib/iotools/ecmwf_macc.py index 4fa8ebd061..fc08eea35f 100644 --- a/pvlib/iotools/ecmwf_macc.py +++ b/pvlib/iotools/ecmwf_macc.py @@ -86,7 +86,7 @@ def get_ecmwf_macc(filename, params, startdate, stopdate, lookup_params=True, key. Please read the documentation in `Access ECMWF Public Datasets `_. Follow the instructions in step 4 and save the ECMWF registration key - as `$HOME\.ecmwfapirc` or set `ECMWF_API_KEY` as the path to the key. + as `$HOME/.ecmwfapirc` or set `ECMWF_API_KEY` as the path to the key. This function returns a daemon thread that runs in the background. Exiting Python will kill this thread, however this thread will not block the main diff --git a/pvlib/location.py b/pvlib/location.py index cb865bd046..afca46ece0 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -66,6 +66,9 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=0, 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): self.tz = tz.zone self.pytz = tz diff --git a/pvlib/tests/conftest.py b/pvlib/tests/conftest.py index b803d04c81..b6cd55dea3 100644 --- a/pvlib/tests/conftest.py +++ b/pvlib/tests/conftest.py @@ -70,14 +70,6 @@ def inner(): requires_ephem = pytest.mark.skipif(not has_ephem, reason='requires ephem') -def pandas_0_17(): - return parse_version(pd.__version__) >= parse_version('0.17.0') - - -needs_pandas_0_17 = pytest.mark.skipif( - not pandas_0_17(), reason='requires pandas 0.17 or greater') - - def numpy_1_10(): return parse_version(np.__version__) >= parse_version('1.10.0') diff --git a/pvlib/tests/iotools/test_psm3.py b/pvlib/tests/iotools/test_psm3.py index 7c8196645a..60e1c79079 100644 --- a/pvlib/tests/iotools/test_psm3.py +++ b/pvlib/tests/iotools/test_psm3.py @@ -4,7 +4,7 @@ import os from pvlib.iotools import psm3 -from conftest import needs_pandas_0_22, DATA_DIR +from conftest import DATA_DIR import numpy as np import pandas as pd import pytest @@ -70,7 +70,6 @@ def assert_psm3_equal(header, data, expected): assert (data.index.tzinfo.zone == 'Etc/GMT%+d' % -header['Time Zone']) -@needs_pandas_0_22 @pytest.mark.flaky(reruns=5, reruns_delay=2) def test_get_psm3_tmy(nrel_api_key): """test get_psm3 with a TMY""" @@ -80,7 +79,6 @@ def test_get_psm3_tmy(nrel_api_key): assert_psm3_equal(header, data, expected) -@needs_pandas_0_22 @pytest.mark.flaky(reruns=5, reruns_delay=2) def test_get_psm3_singleyear(nrel_api_key): """test get_psm3 with a single year""" @@ -90,7 +88,6 @@ def test_get_psm3_singleyear(nrel_api_key): assert_psm3_equal(header, data, expected) -@needs_pandas_0_22 @pytest.mark.flaky(reruns=5, reruns_delay=2) def test_get_psm3_check_leap_day(nrel_api_key): _, data_2012 = psm3.get_psm3(LATITUDE, LONGITUDE, nrel_api_key, @@ -105,7 +102,6 @@ def test_get_psm3_check_leap_day(nrel_api_key): (LATITUDE, LONGITUDE, nrel_api_key, 'bad', 60), (LATITUDE, LONGITUDE, nrel_api_key, '2017', 15), ]) -@needs_pandas_0_22 @pytest.mark.flaky(reruns=5, reruns_delay=2) def test_get_psm3_tmy_errors( latitude, longitude, api_key, names, interval @@ -134,7 +130,6 @@ def io_input(request): return obj -@needs_pandas_0_22 def test_parse_psm3(io_input): """test parse_psm3""" header, data = psm3.parse_psm3(io_input) @@ -142,7 +137,6 @@ def test_parse_psm3(io_input): assert_psm3_equal(header, data, expected) -@needs_pandas_0_22 def test_read_psm3(): """test read_psm3""" header, data = psm3.read_psm3(MANUAL_TEST_DATA) diff --git a/pvlib/tests/test_forecast.py b/pvlib/tests/test_forecast.py index d98deb357c..43b8bd6308 100644 --- a/pvlib/tests/test_forecast.py +++ b/pvlib/tests/test_forecast.py @@ -1,5 +1,4 @@ -from datetime import datetime, timedelta -from pytz import timezone +from datetime import datetime, timedelta, timezone import warnings import pandas as pd @@ -114,7 +113,7 @@ def test_vert_level(): @requires_siphon def test_datetime(): amodel = NAM() - start = datetime.now() + start = datetime.now(tz=timezone.utc) end = start + timedelta(days=1) amodel.get_processed_data(_latitude, _longitude, start, end) @@ -138,7 +137,6 @@ def test_full(): GFS(set_type='full') -@requires_siphon def test_temp_convert(): amodel = GFS() data = pd.DataFrame({'temp_air': [273.15]}) @@ -157,14 +155,19 @@ def test_temp_convert(): # variables=new_variables) -@requires_siphon def test_set_location(): amodel = GFS() latitude, longitude = 32.2, -110.9 - time = datetime.now(timezone('UTC')) + time = 'UTC' amodel.set_location(time, latitude, longitude) +def test_set_query_time_range_tzfail(): + amodel = GFS() + with pytest.raises(TypeError): + amodel.set_query_time_range(datetime.now(), datetime.now()) + + def test_cloud_cover_to_transmittance_linear(): amodel = GFS() assert_allclose(amodel.cloud_cover_to_transmittance_linear(0), 0.75) diff --git a/pvlib/tests/test_location.py b/pvlib/tests/test_location.py index 2deec4c8da..c3c3959985 100644 --- a/pvlib/tests/test_location.py +++ b/pvlib/tests/test_location.py @@ -29,6 +29,7 @@ def test_location_all(): @pytest.mark.parametrize('tz', [ pytz.timezone('US/Arizona'), 'America/Phoenix', -7, -7.0, + datetime.timezone.utc ]) def test_location_tz(tz): Location(32.2, -111, tz) diff --git a/pvlib/tests/test_solarposition.py b/pvlib/tests/test_solarposition.py index d1f446e818..026e28e55c 100644 --- a/pvlib/tests/test_solarposition.py +++ b/pvlib/tests/test_solarposition.py @@ -12,8 +12,7 @@ from pvlib.location import Location from pvlib import solarposition, spa -from conftest import (requires_ephem, needs_pandas_0_17, - requires_spa_c, requires_numba) +from conftest import requires_ephem, requires_spa_c, requires_numba # setup times and locations to be tested. @@ -164,7 +163,6 @@ def test_spa_python_numpy_physical_dst(expected_solpos, golden): assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) -@needs_pandas_0_17 def test_sun_rise_set_transit_spa(expected_rise_set_spa, golden): # solution from NREL SAP web calculator south = Location(-35.0, 0.0, tz='UTC')