From 0746c1ad060741f579f9cdf0c8ec6771ebc4c677 Mon Sep 17 00:00:00 2001 From: Ryan May Date: Mon, 6 Nov 2023 17:20:46 -0700 Subject: [PATCH] MNT: Use tz-aware datetime methods Python 3.12 deprecated UTC tz-naive methods utcnow() and utcfromtimestamp(). This adds a filter for one import-time warning from dateutil. --- docs/conf.py | 4 +-- examples/meteogram_metpy.py | 2 +- pyproject.toml | 7 +++++- src/metpy/io/nexrad.py | 5 ++-- src/metpy/io/text.py | 4 +-- src/metpy/plots/_util.py | 6 ++--- tests/io/test_nexrad.py | 49 +++++++++++++++++++++++-------------- tests/io/test_text.py | 6 ++--- 8 files changed, 50 insertions(+), 33 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 87493d09c4d..e7d94ae3aaf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from datetime import datetime +from datetime import datetime, timezone import os from pathlib import Path import re @@ -128,7 +128,7 @@ # The encoding of source files. # source_encoding = 'utf-8-sig' -cur_date = datetime.utcnow() +cur_date = datetime.now(timezone.utc) # The main toctree document. master_doc = 'index' diff --git a/examples/meteogram_metpy.py b/examples/meteogram_metpy.py index 0dd259467a5..e0bff215de3 100644 --- a/examples/meteogram_metpy.py +++ b/examples/meteogram_metpy.py @@ -46,7 +46,7 @@ def __init__(self, fig, dates, probeid, time=None, axis=0): axis: number that controls the new axis to be plotted (FOR FUTURE) """ if not time: - time = dt.datetime.utcnow() + time = dt.datetime.now(dt.timezone.utc) self.start = dates[0] self.fig = fig self.end = dates[-1] diff --git a/pyproject.toml b/pyproject.toml index 4af0496667a..e3140de250c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,7 +102,12 @@ markers = "xfail_dask: marks tests as expected to fail with Dask arrays" norecursedirs = "build docs .idea" doctest_optionflags = "NORMALIZE_WHITESPACE" mpl-results-path = "test_output" -filterwarnings = ["ignore:numpy.ndarray size changed:RuntimeWarning"] +filterwarnings = [ + "ignore:numpy.ndarray size changed:RuntimeWarning", + # To be removed in the next python-dateutil release. + # See: https://github.com/dateutil/dateutil/issues/1314 + 'ignore:datetime.datetime.utcfromtimestamp\(\) is deprecated:DeprecationWarning:dateutil.tz.tz:37' +] [tool.ruff] line-length = 95 diff --git a/src/metpy/io/nexrad.py b/src/metpy/io/nexrad.py index ff76a1dae9b..50d6c962966 100644 --- a/src/metpy/io/nexrad.py +++ b/src/metpy/io/nexrad.py @@ -6,7 +6,7 @@ import bz2 from collections import defaultdict, namedtuple, OrderedDict import contextlib -import datetime +from datetime import datetime, timezone import logging import pathlib import re @@ -75,7 +75,8 @@ def bzip_blocks_decompress_all(data): def nexrad_to_datetime(julian_date, ms_midnight): """Convert NEXRAD date time format to python `datetime.datetime`.""" # Subtracting one from julian_date is because epoch date is 1 - return datetime.datetime.utcfromtimestamp((julian_date - 1) * day + ms_midnight * milli) + return datetime.fromtimestamp((julian_date - 1) * day + ms_midnight * milli, + tz=timezone.utc) def remap_status(val): diff --git a/src/metpy/io/text.py b/src/metpy/io/text.py index 9ff480190da..af49906be42 100644 --- a/src/metpy/io/text.py +++ b/src/metpy/io/text.py @@ -4,7 +4,7 @@ """Support reading information from various text file formats.""" import contextlib -from datetime import datetime +from datetime import datetime, timezone import re import string @@ -95,7 +95,7 @@ def parse_wpc_surface_bulletin(bulletin, year=None): text = file.read().decode('utf-8') parsed_text = [] - valid_time = datetime.utcnow() + valid_time = datetime.now(timezone.utc) for parts in _regroup_lines(text.splitlines()): # A single file may have multiple sets of data that are valid at different times. Set # the valid_time string that will correspond to all the following lines parsed, until diff --git a/src/metpy/plots/_util.py b/src/metpy/plots/_util.py index 1135d538e65..5992dc4f8c4 100644 --- a/src/metpy/plots/_util.py +++ b/src/metpy/plots/_util.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: BSD-3-Clause """Utilities for use in making plots.""" -from datetime import datetime +from datetime import datetime, timezone from matplotlib.collections import LineCollection import matplotlib.patheffects as mpatheffects @@ -24,7 +24,7 @@ def add_timestamp(ax, time=None, x=0.99, y=-0.04, ha='right', high_contrast=Fals ax : `matplotlib.axes.Axes` The `Axes` instance used for plotting time : `datetime.datetime` (or any object with a compatible ``strftime`` method) - Specific time to be plotted - datetime.utcnow will be use if not specified + Specific time to be plotted - ``datetime.now(UTC)`` will be use if not specified x : float Relative x position on the axes of the timestamp y : float @@ -52,7 +52,7 @@ def add_timestamp(ax, time=None, x=0.99, y=-0.04, ha='right', high_contrast=Fals text_args = {} text_args.update(**kwargs) if not time: - time = datetime.utcnow() + time = datetime.now(timezone.utc) timestr = time.strftime(time_format) # If we don't have a time string after that, assume xarray/numpy and see if item if not isinstance(timestr, str): diff --git a/tests/io/test_nexrad.py b/tests/io/test_nexrad.py index 2d227021ac9..d7fa01e5267 100644 --- a/tests/io/test_nexrad.py +++ b/tests/io/test_nexrad.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: BSD-3-Clause """Test the `nexrad` module.""" import contextlib -from datetime import datetime +from datetime import datetime, timezone from io import BytesIO import logging from pathlib import Path @@ -26,18 +26,24 @@ # KTLX 20150530 has missing segments for message 18, which was causing exception # KICX has message type 29 (MDM) # KVWX and KLTX have some legacy "quirks"; KLTX was crashing the parser -level2_files = [('KTLX20130520_201643_V06.gz', datetime(2013, 5, 20, 20, 16, 46), 17, 4, 6, 0), - ('KTLX19990503_235621.gz', datetime(1999, 5, 3, 23, 56, 21), 16, 1, 3, 0), - ('Level2_KFTG_20150430_1419.ar2v', datetime(2015, 4, 30, 14, 19, 11), - 12, 4, 6, 0), - ('KTLX20150530_000802_V06.bz2', datetime(2015, 5, 30, 0, 8, 3), 14, 4, 6, 2), - ('KICX_20170712_1458', datetime(2017, 7, 12, 14, 58, 5), 14, 4, 6, 1), - ('TDAL20191021021543V08.raw.gz', datetime(2019, 10, 21, 2, 15, 43), 10, 1, - 3, 0), - ('Level2_FOP1_20191223_003655.ar2v', datetime(2019, 12, 23, 0, 36, 55, 649000), - 16, 5, 7, 0), - ('KVWX_20050626_221551.gz', datetime(2005, 6, 26, 22, 15, 51), 11, 1, 3, 21), - ('KLTX20050329_100015.gz', datetime(2005, 3, 29, 10, 0, 15), 11, 1, 3, 21)] +level2_files = [('KTLX20130520_201643_V06.gz', + datetime(2013, 5, 20, 20, 16, 46, tzinfo=timezone.utc), 17, 4, 6, 0), + ('KTLX19990503_235621.gz', + datetime(1999, 5, 3, 23, 56, 21, tzinfo=timezone.utc), 16, 1, 3, 0), + ('Level2_KFTG_20150430_1419.ar2v', + datetime(2015, 4, 30, 14, 19, 11, tzinfo=timezone.utc), 12, 4, 6, 0), + ('KTLX20150530_000802_V06.bz2', + datetime(2015, 5, 30, 0, 8, 3, tzinfo=timezone.utc), 14, 4, 6, 2), + ('KICX_20170712_1458', + datetime(2017, 7, 12, 14, 58, 5, tzinfo=timezone.utc), 14, 4, 6, 1), + ('TDAL20191021021543V08.raw.gz', + datetime(2019, 10, 21, 2, 15, 43, tzinfo=timezone.utc), 10, 1, 3, 0), + ('Level2_FOP1_20191223_003655.ar2v', + datetime(2019, 12, 23, 0, 36, 55, 649000, tzinfo=timezone.utc), 16, 5, 7, 0), + ('KVWX_20050626_221551.gz', + datetime(2005, 6, 26, 22, 15, 51, tzinfo=timezone.utc), 11, 1, 3, 21), + ('KLTX20050329_100015.gz', + datetime(2005, 3, 29, 10, 0, 15, tzinfo=timezone.utc), 11, 1, 3, 21)] # ids here fixes how things are presented in pycharm @@ -99,7 +105,8 @@ def test_msg15(): f = Level2File(get_test_data('KTLX20130520_201643_V06.gz', as_file_obj=False)) data = f.clutter_filter_map['data'] assert isinstance(data[0][0], list) - assert f.clutter_filter_map['datetime'] == datetime(2013, 5, 19, 5, 15, 0, 0) + assert f.clutter_filter_map['datetime'] == datetime(2013, 5, 19, 5, 15, 0, 0, + tzinfo=timezone.utc) def test_single_chunk(caplog): @@ -154,9 +161,12 @@ def test_level3_files(fname): def test_basic(): """Test reading one specific NEXRAD NIDS file based on the filename.""" f = Level3File(get_test_data('nids/Level3_FFC_N0Q_20140407_1805.nids', as_file_obj=False)) - assert f.metadata['prod_time'].replace(second=0) == datetime(2014, 4, 7, 18, 5) - assert f.metadata['vol_time'].replace(second=0) == datetime(2014, 4, 7, 18, 5) - assert f.metadata['msg_time'].replace(second=0) == datetime(2014, 4, 7, 18, 6) + assert f.metadata['prod_time'].replace(second=0) == datetime(2014, 4, 7, 18, 5, + tzinfo=timezone.utc) + assert f.metadata['vol_time'].replace(second=0) == datetime(2014, 4, 7, 18, 5, + tzinfo=timezone.utc) + assert f.metadata['msg_time'].replace(second=0) == datetime(2014, 4, 7, 18, 6, + tzinfo=timezone.utc) assert f.filename == get_test_data('nids/Level3_FFC_N0Q_20140407_1805.nids', as_file_obj=False) @@ -203,7 +213,7 @@ def test_tdwr(): def test_dhr(): """Test reading a time field for DHR product.""" f = Level3File(get_test_data('nids/KOUN_SDUS54_DHRTLX_201305202016')) - assert f.metadata['avg_time'] == datetime(2013, 5, 20, 20, 18) + assert f.metadata['avg_time'] == datetime(2013, 5, 20, 20, 18, tzinfo=timezone.utc) def test_fobj(): @@ -232,7 +242,8 @@ def test_power_removed_control(): assert f.metadata['rpg_cut_num'] == 1 assert f.metadata['cmd_generated'] == 0 assert f.metadata['el_angle'] == -0.2 - assert f.metadata['clutter_filter_map_dt'] == datetime(2020, 8, 17, 4, 16) + assert f.metadata['clutter_filter_map_dt'] == datetime(2020, 8, 17, 4, 16, + tzinfo=timezone.utc) assert f.metadata['compression'] == 1 assert f.sym_block[0][0] diff --git a/tests/io/test_text.py b/tests/io/test_text.py index d66a2c73cb8..81c1a38e4ea 100644 --- a/tests/io/test_text.py +++ b/tests/io/test_text.py @@ -2,7 +2,7 @@ # Distributed under the terms of the BSD 3-Clause License. # SPDX-License-Identifier: BSD-3-Clause """Test text handling functions.""" -from datetime import datetime +from datetime import datetime, timezone import numpy as np @@ -34,7 +34,7 @@ def test_parse_wpc_surface_bulletin_highres(): assert df.geometry[47] == sgeom.LineString([[-100.5, 32.4], [-101.0, 31.9], [-101.9, 31.5], [-102.9, 31.2]]) - assert all(df.valid == datetime(2021, 6, 28, 18, 0, 0)) + assert all(df.valid == datetime(2021, 6, 28, 18, 0, 0, tzinfo=timezone.utc)) @needs_module('shapely') @@ -60,4 +60,4 @@ def test_parse_wpc_surface_bulletin(): assert df.geometry[47] == sgeom.LineString([[-100, 32], [-101, 32], [-102, 32], [-103, 31]]) - assert all(df.valid == datetime(2021, 6, 28, 18, 0, 0)) + assert all(df.valid == datetime(2021, 6, 28, 18, 0, 0, tzinfo=timezone.utc))