diff --git a/.github/workflows/docs-conda.yml b/.github/workflows/docs-conda.yml index 1b31888cee9..59bfd530bfb 100644 --- a/.github/workflows/docs-conda.yml +++ b/.github/workflows/docs-conda.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false matrix: include: - - python-version: '3.10' + - python-version: 3.11 os: Windows - python-version: 3.11 os: macOS diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4ddc74c4bde..e66885501a2 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.10', 3.11, 3.12] + python-version: [3.11, 3.12] check-links: [false] include: - python-version: 3.13 diff --git a/.github/workflows/tests-conda.yml b/.github/workflows/tests-conda.yml index ab68d65de69..2552266f9ec 100644 --- a/.github/workflows/tests-conda.yml +++ b/.github/workflows/tests-conda.yml @@ -28,11 +28,9 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.10', 3.13] + python-version: [3.11, 3.13] os: [macOS, Windows] include: - - python-version: 3.11 - os: Windows - python-version: 3.12 os: macOS diff --git a/.github/workflows/tests-pypi.yml b/.github/workflows/tests-pypi.yml index da700de299a..13b090dbecc 100644 --- a/.github/workflows/tests-pypi.yml +++ b/.github/workflows/tests-pypi.yml @@ -25,13 +25,13 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.10', 3.11, 3.12, 3.13] + python-version: [3.11, 3.12, 3.13] dep-versions: [Latest] no-extras: [''] include: - - python-version: '3.10' + - python-version: '3.11' dep-versions: Minimum - - python-version: '3.10' + - python-version: '3.11' dep-versions: Minimum no-extras: 'No Extras' - python-version: 3.13 diff --git a/.python-version b/.python-version deleted file mode 100644 index c8cfe395918..00000000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.10 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0a074a55bf6..d3f06f5bf5f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -232,8 +232,9 @@ MetPy documentation via the `gh-pages` branch on GitHub. Unit tests are the lifeblood of the project, as it ensures that we can continue to add and change the code and stay confident that things have not broken. Running the tests requires -``pytest``, which is easily available through ``conda`` or ``pip``. It was also installed if -you made our default ``devel`` environment. +``pytest``, which is easily available through ``conda``, ``pip``, or after installing the +`test` extras with `uv`. It was also installed if you made our default ``devel`` +environment. ### Running Tests diff --git a/README.md b/README.md index 1c23d63552a..b5ae4456fc4 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ on a future ``1.x`` version. For additional MetPy examples not included in this repository, please see the [MetPy Cookbook on Project Pythia](https://projectpythia.org/metpy-cookbook/index.html). -We support Python >= 3.10. +We support Python >= 3.11. Need Help? ---------- diff --git a/docs/index.rst b/docs/index.rst index 04bbe5f5952..64eab81f730 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,7 +14,7 @@ MetPy ===== MetPy is a collection of tools in Python for reading, visualizing, and performing calculations -with weather data. MetPy supports Python >= 3.10 and is freely available under a permissive +with weather data. MetPy supports Python >= 3.11 and is freely available under a permissive `open source license `_. If you're new to MetPy, check out our :doc:`Getting Started ` guide. diff --git a/docs/userguide/installguide.rst b/docs/userguide/installguide.rst index e0876768f32..b17e6132454 100644 --- a/docs/userguide/installguide.rst +++ b/docs/userguide/installguide.rst @@ -7,7 +7,7 @@ Requirements ------------ In general, MetPy tries to support minor versions of dependencies released within the last two years. For Python itself, that generally means supporting the last two minor releases; MetPy -currently supports Python >= 3.10. +currently supports Python >= 3.11. .. literalinclude:: ../../pyproject.toml :start-at: matplotlib diff --git a/examples/meteogram_metpy.py b/examples/meteogram_metpy.py index 90bb8784de0..e075d6a64c6 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.now(dt.timezone.utc) + time = dt.datetime.now(dt.UTC) self.start = dates[0] self.fig = fig self.end = dates[-1] diff --git a/pyproject.toml b/pyproject.toml index 33bf3be94c4..ddb054f114f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Matplotlib", "Programming Language :: Python", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -24,15 +23,15 @@ classifiers = [ "Operating System :: OS Independent", "License :: OSI Approved :: BSD License" ] -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ - "matplotlib>=3.5.0", - "numpy>=1.23.0", - "pandas>=1.4.0", - "pint>=0.17", + "matplotlib>=3.7.0", + "numpy>=1.25.0", + "pandas>=2.1.0", + "pint>=0.22", "pooch>=1.2.0", - "pyproj>=3.3.0", - "scipy>=1.8.0", + "pyproj>=3.4.0", + "scipy>=1.10.0", "traitlets>=5.1.0", "xarray>=2022.6.0" ] diff --git a/src/metpy/io/metar.py b/src/metpy/io/metar.py index 7cd7b22afcf..465319e811a 100644 --- a/src/metpy/io/metar.py +++ b/src/metpy/io/metar.py @@ -5,7 +5,7 @@ # Import the necessary libraries from collections import namedtuple import contextlib -from datetime import datetime, timezone +from datetime import datetime, UTC import warnings import numpy as np @@ -433,7 +433,7 @@ def _metars_to_dataframe(metar_iter, *, year=None, month=None): # Defaults year and/or month to present reported date if not provided if year is None or month is None: - now = datetime.now(timezone.utc) + now = datetime.now(UTC) year = now.year if year is None else year month = now.month if month is None else month diff --git a/src/metpy/io/nexrad.py b/src/metpy/io/nexrad.py index ee0041f0356..2bacef97801 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 -from datetime import datetime, timezone +from datetime import datetime, UTC import logging import pathlib import re @@ -76,7 +76,7 @@ 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.fromtimestamp((julian_date - 1) * day + ms_midnight * milli, - tz=timezone.utc).replace(tzinfo=None) + tz=UTC).replace(tzinfo=None) def remap_status(val): diff --git a/src/metpy/io/text.py b/src/metpy/io/text.py index a922f34cbda..262cf0ec8a5 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, timezone +from datetime import datetime, UTC import re import string @@ -102,7 +102,7 @@ def parse_wpc_surface_bulletin(bulletin, year=None): text = file.read().decode('utf-8') parsed_text = [] - valid_time = datetime.now(timezone.utc).replace(tzinfo=None) + valid_time = datetime.now(UTC).replace(tzinfo=None) 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 5992dc4f8c4..1b3cc099151 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, timezone +from datetime import datetime, UTC from matplotlib.collections import LineCollection import matplotlib.patheffects as mpatheffects @@ -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.now(timezone.utc) + time = datetime.now(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/src/metpy/plots/station_plot.py b/src/metpy/plots/station_plot.py index 971946a9456..9cf4f519d4c 100644 --- a/src/metpy/plots/station_plot.py +++ b/src/metpy/plots/station_plot.py @@ -371,13 +371,7 @@ def _make_kwargs(self, kwargs): def _to_string_list(vals, fmt): """Convert a sequence of values to a list of strings.""" if fmt is None: - import sys - if sys.version_info >= (3, 11): - fmt = 'z.0f' - else: - def fmt(s): - """Perform default formatting with no decimal places and no negative 0.""" - return format(round(s, 0) + 0., '.0f') + fmt = 'z.0f' if not callable(fmt): def formatter(s): """Turn a format string into a callable.""" diff --git a/src/metpy/remote/aws.py b/src/metpy/remote/aws.py index 9534e91c636..69f5f771f01 100644 --- a/src/metpy/remote/aws.py +++ b/src/metpy/remote/aws.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: BSD-3-Clause """Tools for reading known collections of data that are hosted on Amazon Web Services (AWS).""" import bisect -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta, UTC import itertools from pathlib import Path import shutil @@ -18,7 +18,7 @@ def ensure_timezone(dt): """Add UTC timezone if no timezone present.""" - return dt.replace(tzinfo=timezone.utc) if dt.tzinfo is None else dt + return dt.replace(tzinfo=UTC) if dt.tzinfo is None else dt class AWSProduct: @@ -239,7 +239,7 @@ def _build_key(self, site, prod_id, dt, depth=None): def dt_from_key(self, key): # noqa: D102 # Docstring inherited return datetime.strptime(key.split(self.delimiter, maxsplit=2)[-1], - '%Y_%m_%d_%H_%M_%S').replace(tzinfo=timezone.utc) + '%Y_%m_%d_%H_%M_%S').replace(tzinfo=UTC) def get_range(self, site, prod_id, start, end): """Yield products within a particular date/time range. @@ -317,7 +317,7 @@ def get_product(self, site, prod_id, dt=None): product_ids, sites, get_range """ - dt = datetime.now(timezone.utc) if dt is None else ensure_timezone(dt) + dt = datetime.now(UTC) if dt is None else ensure_timezone(dt) # We work with a list of keys/prefixes that we iteratively find that bound our target # key. To start, this only contains the site and product. @@ -389,7 +389,7 @@ def sites(self, dt=None): """ if dt is None: - dt = datetime.now(timezone.utc) + dt = datetime.now(UTC) prefix = self._build_key('', dt, depth=3) + self.delimiter return [item.split('/')[-2] for item in self.common_prefixes(prefix)] @@ -401,7 +401,7 @@ def _build_key(self, site, dt, depth=None): def dt_from_key(self, key): # noqa: D102 # Docstring inherited return datetime.strptime(key.rsplit(self.delimiter, maxsplit=1)[-1][4:19], - '%Y%m%d_%H%M%S').replace(tzinfo=timezone.utc) + '%Y%m%d_%H%M%S').replace(tzinfo=UTC) def get_range(self, site, start, end): """Yield products within a particular date/time range. @@ -450,7 +450,7 @@ def get_product(self, site, dt=None): sites, get_range """ - dt = datetime.now(timezone.utc) if dt is None else ensure_timezone(dt) + dt = datetime.now(UTC) if dt is None else ensure_timezone(dt) search_key = self._build_key(site, dt) prefix = search_key.split('_')[0] objs = (self.objects(prefix) if self.include_mdm else @@ -530,7 +530,7 @@ def _subprod_prefix(self, prefix, mode, band): def dt_from_key(self, key): # noqa: D102 # Docstring inherited start_time = key.split('_')[-3] - return datetime.strptime(start_time[:-1], 's%Y%j%H%M%S').replace(tzinfo=timezone.utc) + return datetime.strptime(start_time[:-1], 's%Y%j%H%M%S').replace(tzinfo=UTC) def get_product(self, product, dt=None, mode=None, band=None): """Get a product from the archive. @@ -556,7 +556,7 @@ def get_product(self, product, dt=None, mode=None, band=None): product_ids, get_range """ - dt = datetime.now(timezone.utc) if dt is None else ensure_timezone(dt) + dt = datetime.now(UTC) if dt is None else ensure_timezone(dt) time_prefix = self._build_time_prefix(product, dt) prod_prefix = self._subprod_prefix(time_prefix, mode, band) return self._closest_result(self.objects(prod_prefix), dt) @@ -645,7 +645,7 @@ def dt_from_key(self, key): # noqa: D102 # Docstring inherited # GRAP_v100_GFS_2025021212_f000_f240_06.nc dt = key.split('/')[-1].split('_')[3] - return datetime.strptime(dt, '%Y%m%d%H').replace(tzinfo=timezone.utc) + return datetime.strptime(dt, '%Y%m%d%H').replace(tzinfo=UTC) def get_product(self, model, dt=None, version=None, init=None): """Get a product from the archive. @@ -673,7 +673,7 @@ def get_product(self, model, dt=None, version=None, init=None): get_range """ - dt = datetime.now(timezone.utc) if dt is None else ensure_timezone(dt) + dt = datetime.now(UTC) if dt is None else ensure_timezone(dt) model_id = self._model_id(model, version, init) search_key = self._build_key(model_id, dt) prefix = search_key.rsplit('_', maxsplit=4)[0] diff --git a/src/metpy/units.py b/src/metpy/units.py index f80928f5796..f98d6f18f93 100644 --- a/src/metpy/units.py +++ b/src/metpy/units.py @@ -75,10 +75,6 @@ def setup_registry(reg): if pre not in reg.preprocessors: reg.preprocessors.append(pre) - # Add a percent unit if it's not already present, it was added in 0.21 - if 'percent' not in reg: - reg.define('percent = 0.01 = %') - # Define commonly encountered units not defined by pint reg.define('degrees_north = degree = degrees_N = degreesN = degree_north = degree_N ' '= degreeN') diff --git a/tests/calc/test_indices.py b/tests/calc/test_indices.py index d849174aaa8..19430da646d 100644 --- a/tests/calc/test_indices.py +++ b/tests/calc/test_indices.py @@ -12,8 +12,7 @@ from metpy.calc import (bulk_shear, bunkers_storm_motion, corfidi_storm_motion, critical_angle, mean_pressure_weighted, precipitable_water, significant_tornado, supercell_composite, weighted_continuous_average) -from metpy.testing import (assert_almost_equal, assert_array_almost_equal, get_upper_air_data, - version_check) +from metpy.testing import assert_almost_equal, assert_array_almost_equal, get_upper_air_data from metpy.units import concatenate, units @@ -131,7 +130,6 @@ def test_weighted_continuous_average(): assert_almost_equal(v, 6.900543760612305 * units('m/s'), 7) -@pytest.mark.xfail(condition=version_check('pint<0.21'), reason='hgrecco/pint#1593') def test_weighted_continuous_average_temperature(): """Test pressure-weighted mean temperature function with vertical interpolation.""" data = get_upper_air_data(datetime(2016, 5, 22, 0), 'DDC') diff --git a/tests/plots/test_skewt.py b/tests/plots/test_skewt.py index 4f4b801a846..587b0b602e0 100644 --- a/tests/plots/test_skewt.py +++ b/tests/plots/test_skewt.py @@ -321,8 +321,7 @@ def test_hodograph_api(): return fig -@pytest.mark.mpl_image_compare( - remove_text=True, tolerance=0.6 if version_check('matplotlib==3.5') else 0.) +@pytest.mark.mpl_image_compare(remove_text=True, tolerance=0.) def test_hodograph_units(): """Test passing quantities to Hodograph.""" fig = plt.figure(figsize=(9, 9)) diff --git a/tests/plots/test_util.py b/tests/plots/test_util.py index 96b366cd878..95911d97f61 100644 --- a/tests/plots/test_util.py +++ b/tests/plots/test_util.py @@ -11,7 +11,7 @@ import xarray as xr from metpy.plots import add_metpy_logo, add_timestamp, add_unidata_logo, convert_gempak_color -from metpy.testing import autoclose_figure, get_test_data, version_check +from metpy.testing import autoclose_figure, get_test_data @pytest.mark.mpl_image_compare(tolerance=2.638, remove_text=True) @@ -90,9 +90,7 @@ def test_add_logo_invalid_size(): add_metpy_logo(fig, size='jumbo') -@pytest.mark.mpl_image_compare( - tolerance=1.072 if version_check('matplotlib<3.5') else 0, - remove_text=True) +@pytest.mark.mpl_image_compare(tolerance=0, remove_text=True) def test_gempak_color_image_compare(): """Test creating a plot with all the GEMPAK colors.""" c = range(32) @@ -111,9 +109,7 @@ def test_gempak_color_image_compare(): return fig -@pytest.mark.mpl_image_compare( - tolerance=1.215 if version_check('matplotlib<3.5') else 0, - remove_text=True) +@pytest.mark.mpl_image_compare(tolerance=0, remove_text=True) def test_gempak_color_xw_image_compare(): """Test creating a plot with all the GEMPAK colors using xw style.""" c = range(32)