diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 806fa0d0800..dec9a0ac4a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: apt: - libopenjp2-7 envs: | - - linux: py311 + - linux: py312 secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -47,8 +47,9 @@ jobs: brew: - openjpeg envs: | - - windows: py310 - - macos: py39 + - windows: py311 + - macos: py310 + - linux: py39 - linux: py39-oldestdeps secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -178,9 +179,9 @@ jobs: test_command: 'pytest -p no:warnings --doctest-rst -m "not mpl_image_compare" --pyargs sunpy' submodules: false targets: | - - cp3{9,10,11}-manylinux*_x86_64 - - cp3{9,10,11}-macosx_x86_64 - - cp3{9,10,11}-macosx_arm64 + - cp3{9,10,11,12}-manylinux*_x86_64 + - cp3{9,10,11,12}-macosx_x86_64 + - cp3{9,10,11,12}-macosx_arm64 secrets: pypi_token: ${{ secrets.pypi_token }} diff --git a/changelog/7351.feature.rst b/changelog/7351.feature.rst new file mode 100644 index 00000000000..d35054f41a0 --- /dev/null +++ b/changelog/7351.feature.rst @@ -0,0 +1 @@ +Added testing and explicit support for Python 3.12. diff --git a/setup.cfg b/setup.cfg index b2b07e73ca7..0c4643acb97 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Topic :: Scientific/Engineering :: Physics [options] @@ -60,7 +61,7 @@ image = scipy>=1.7.0,!=1.10.0 jpeg2000 = glymur>=0.9.1,!=0.9.5 - lxml>=4.8.0 + lxml>=4.8.0,!=5.0.0 map = matplotlib>=3.5.0 mpl-animators>=1.0.0 @@ -236,6 +237,8 @@ filterwarnings = ignore:.*module is deprecated, as it was designed for internal use # This is raised when the VSO redirects and we do not want this to stop the CI ignore::sunpy.util.exceptions.SunpyConnectionWarning + # Can be removed when https://github.com/dateutil/dateutil/issues/1314 is resolved + ignore:datetime.datetime.utcfromtimestamp():DeprecationWarning [pycodestyle] max_line_length = 110 @@ -273,7 +276,6 @@ omit = sunpy/cython_version* sunpy/*setup* sunpy/extern/* - sunpy/*/tests/* sunpy/version* sunpy/__init__* sunpy/data/sample.py @@ -283,7 +285,6 @@ omit = */sunpy/cython_version* */sunpy/*setup* */sunpy/extern/* - */sunpy/*/tests/* */sunpy/version* */sunpy/__init__* */sunpy/data/sample.py diff --git a/sunpy/map/header_helper.py b/sunpy/map/header_helper.py index f0c19cd908f..6456a612a3f 100644 --- a/sunpy/map/header_helper.py +++ b/sunpy/map/header_helper.py @@ -408,7 +408,33 @@ def make_heliographic_header(date, observer_coordinate, shape, *, frame, project >>> observer = get_earth(date) >>> header = make_heliographic_header(date, observer, [90, 180], frame='carrington') >>> header - MetaDict([('wcsaxes', 2), ('crpix1', 90.5), ('crpix2', 45.5), ('cdelt1', 2.0), ('cdelt2', 2.0), ('cunit1', 'deg'), ('cunit2', 'deg'), ('ctype1', 'CRLN-CAR'), ('ctype2', 'CRLT-CAR'), ('crval1', 0.0), ('crval2', 0.0), ('lonpole', 0.0), ('latpole', 90.0), ('mjdref', 0.0), ('date-obs', '2020-01-01T12:00:00.000'), ('rsun_ref', 695700000.0), ('dsun_obs', 147096975776.97), ('hgln_obs', 0.0), ('hglt_obs', -3.0011725838606), ('naxis', 2), ('naxis1', 180), ('naxis2', 90), ('pc1_1', 1.0), ('pc1_2', -0.0), ('pc2_1', 0.0), ('pc2_2', 1.0), ('rsun_obs', 975.5398432033492)]) + MetaDict([('wcsaxes': '2') + ('crpix1': '90.5') + ('crpix2': '45.5') + ('cdelt1': '2.0') + ('cdelt2': '2.0') + ('cunit1': 'deg') + ('cunit2': 'deg') + ('ctype1': 'CRLN-CAR') + ('ctype2': 'CRLT-CAR') + ('crval1': '0.0') + ('crval2': '0.0') + ('lonpole': '0.0') + ('latpole': '90.0') + ('mjdref': '0.0') + ('date-obs': '2020-01-01T12:00:00.000') + ('rsun_ref': '695700000.0') + ('dsun_obs': '147096975776.97') + ('hgln_obs': '0.0') + ('hglt_obs': '-3.0011725838606') + ('naxis': '2') + ('naxis1': '180') + ('naxis2': '90') + ('pc1_1': '1.0') + ('pc1_2': '-0.0') + ('pc2_1': '0.0') + ('pc2_2': '1.0') + ('rsun_obs': '975.53984320334... .. minigallery:: sunpy.map.make_heliographic_header """ diff --git a/sunpy/net/dataretriever/sources/tests/test_noaa.py b/sunpy/net/dataretriever/sources/tests/test_noaa.py index 60f43d951b9..49a36b55ab4 100644 --- a/sunpy/net/dataretriever/sources/tests/test_noaa.py +++ b/sunpy/net/dataretriever/sources/tests/test_noaa.py @@ -129,8 +129,10 @@ def test_fetch(mock_wait, mock_search, mock_enqueue, tmp_path, indices_client): # Downloader.enqueue_file method with the correct arguments. Everything # that happens after this point should either be tested in the # GenericClient tests or in parfive itself. - assert mock_enqueue.called_once_with(("https://services.swpc.noaa.gov/json/solar-cycle/observed-solar-cycle-indices.json", - path / "observed-solar-cycle-indices.json")) + mock_enqueue.assert_called_once_with( + Time("2012-10-04 00:00:00.000", "2012-10-06 00:00:00.000"), + Instrument("noaa-indices") + ) @mock.patch('sunpy.net.dataretriever.sources.noaa.NOAAIndicesClient.search', @@ -151,8 +153,10 @@ def test_fido(mock_wait, mock_search, mock_enqueue, tmp_path, indices_client): # Downloader.enqueue_file method with the correct arguments. Everything # that happens after this point should either be tested in the # GenericClient tests or in parfive itself. - assert mock_enqueue.called_once_with(("https://services.swpc.noaa.gov/json/solar-cycle/observed-solar-cycle-indices.json", - path / "observed-solar-cycle-indices.json")) + mock_enqueue.assert_called_once_with( + Time('2012-10-04 00:00:00.000', '2012-10-06 00:00:00.000'), + Instrument("noaa-indices") + ) @no_vso diff --git a/sunpy/net/jsoc/tests/test_jsoc.py b/sunpy/net/jsoc/tests/test_jsoc.py index 6070cb4812c..0fb98a94d6d 100644 --- a/sunpy/net/jsoc/tests/test_jsoc.py +++ b/sunpy/net/jsoc/tests/test_jsoc.py @@ -321,7 +321,7 @@ def test_row_and_warning(mocker, client, jsoc_response_double): request_data = mocker.patch("sunpy.net.jsoc.jsoc.JSOCClient.request_data") with pytest.warns(SunpyUserWarning): client.fetch(jsoc_response_double[0], sleep=0) - assert request_data.called_once_with(jsoc_response_double[0].as_table()) + request_data.assert_called_once_with(jsoc_response_double[0].as_table()) @pytest.mark.remote_data diff --git a/sunpy/net/tests/strategies.py b/sunpy/net/tests/strategies.py index 39f54fc219f..ac02ebb23d9 100644 --- a/sunpy/net/tests/strategies.py +++ b/sunpy/net/tests/strategies.py @@ -15,14 +15,14 @@ from sunpy.net import attrs as a from sunpy.time import TimeRange, parse_time -TimesLeapsecond = sampled_from((Time('2015-06-30T23:59:60'), +TIME_LEAP_SECONDS = sampled_from((Time('2015-06-30T23:59:60'), Time('2012-06-30T23:59:60'))) @st.composite def Times(draw, max_value, min_value): time = one_of(datetimes(max_value=max_value, min_value=min_value), - TimesLeapsecond) + TIME_LEAP_SECONDS) time = Time(draw(time)) @@ -74,7 +74,7 @@ def online_instruments(): @st.composite def time_attr(draw, time=Times( - max_value=datetime.datetime(datetime.datetime.utcnow().year, 1, 1, 0, 0), + max_value=datetime.datetime(datetime.datetime.now(datetime.timezone.utc).year, 1, 1, 0, 0), min_value=datetime.datetime(1981, 1, 1, 0, 0)), delta=TimeDelta()): """ @@ -90,7 +90,7 @@ def time_attr(draw, time=Times( @st.composite def goes_time(draw, time=Times( - max_value=datetime.datetime(datetime.datetime.utcnow().year, 1, 1, 0, 0), + max_value=datetime.datetime(datetime.datetime.now(datetime.timezone.utc).year, 1, 1, 0, 0), min_value=datetime.datetime(1981, 1, 1, 0, 0)), delta=TimeDelta()): """ @@ -116,7 +116,7 @@ def goes_time(draw, time=Times( @st.composite def srs_time(draw, time=Times( - max_value=datetime.datetime.now(), + max_value=datetime.datetime.now(datetime.timezone.utc), min_value=datetime.datetime(1996, 1, 1)), delta=TimeDelta()): t1 = draw(time) diff --git a/sunpy/time/tests/test_time.py b/sunpy/time/tests/test_time.py index b5156d44ad7..4d0251deb51 100644 --- a/sunpy/time/tests/test_time.py +++ b/sunpy/time/tests/test_time.py @@ -1,4 +1,4 @@ -from datetime import date, datetime +from datetime import date, datetime, timezone import numpy as np import pandas @@ -352,7 +352,7 @@ def test_parse_time_list_3(): def test_is_time(): - assert time.is_time(datetime.utcnow()) is True + time.is_time(datetime.now(timezone.utc)) is True assert time.is_time('2017-02-14 08:08:12.999') is True assert time.is_time(Time.now()) is True diff --git a/sunpy/timeseries/metadata.py b/sunpy/timeseries/metadata.py index 947bd38a2dc..c54a2f6008d 100644 --- a/sunpy/timeseries/metadata.py +++ b/sunpy/timeseries/metadata.py @@ -72,7 +72,7 @@ class TimeSeriesMetaData: >>> md.find(parse_time('2012-06-01T21:08:12')).values() ['eve_val', 'goes_val'] >>> md.find(parse_time('2012-06-01T21:08:12')).metas - [MetaDict([('goes_key', 'goes_val')]), MetaDict([('eve_key', 'eve_val')])] + [MetaDict([('goes_key': 'goes_val')]), MetaDict([('eve_key': 'eve_val')])] >>> md.find(parse_time('2012-06-01T21:08:12'), 'GOES') |-------------------------------------------------------------------------------------------------| |TimeRange | Columns | Meta | diff --git a/sunpy/util/metadata.py b/sunpy/util/metadata.py index 213977d5486..74b19ae769a 100644 --- a/sunpy/util/metadata.py +++ b/sunpy/util/metadata.py @@ -68,7 +68,10 @@ def __init__(self, *args, save_original=True): self._original_meta = MetaDict(*args, save_original=False) def __str__(self): - return '\n'.join([f'{key}: {item}' for key, item in self.items()]) + return '\n'.join([f"('{key}': '{item}')" for key, item in self.items()]) + + def __repr__(self): + return f"{self.__class__.__name__}([{self}])" # Deliberately a property to prevent external modification @property diff --git a/tox.ini b/tox.ini index 95fcc997256..31c76210f23 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] min_version = 4.0 envlist = - py{39,310,311}{,-oldestdeps,-devdeps,-online,-figure,-conda} + py{39,310,311,312}{,-oldestdeps,-devdeps,-online,-figure,-conda} build_docs{,-gallery} codestyle base_deps