diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 84f4d5f6..7cdbde29 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,7 +32,7 @@ jobs: apt: - graphviz - linux: py311-devdeps - - linux: py38-oldestdeps + - linux: py39-oldestdeps publish: needs: [tests] diff --git a/changelog/232.feature.rst b/changelog/232.feature.rst new file mode 100644 index 00000000..9cb51dd9 --- /dev/null +++ b/changelog/232.feature.rst @@ -0,0 +1 @@ +Drop support for Python 3.8 in line with `NEP 29 _`. diff --git a/changelog/232.trivial.rst b/changelog/232.trivial.rst new file mode 100644 index 00000000..57778708 --- /dev/null +++ b/changelog/232.trivial.rst @@ -0,0 +1,2 @@ +Internal improvements to how the data are loaded from the collection of FITS files. +This should have no user facing effects, but provides a foundation for future performance work. diff --git a/dkist/conftest.py b/dkist/conftest.py index b6a78ae6..8f4d71c8 100644 --- a/dkist/conftest.py +++ b/dkist/conftest.py @@ -24,8 +24,9 @@ @pytest.fixture def array(): - shape = np.random.randint(10, 100, size=2) - x = np.ones(shape) + 10 + shape = 2**np.random.randint(2, 7, size=2) + x = np.ones(np.prod(shape)) + 10 + x = x.reshape(shape) return da.from_array(x, tuple(shape)) diff --git a/dkist/dataset/dataset.py b/dkist/dataset/dataset.py index 70b32bb7..35a076cb 100644 --- a/dkist/dataset/dataset.py +++ b/dkist/dataset/dataset.py @@ -1,12 +1,7 @@ -import sys +import importlib.resources as importlib_resources from pathlib import Path from textwrap import dedent -if sys.version_info < (3, 9): - import importlib_resources -else: - import importlib.resources as importlib_resources - from jsonschema.exceptions import ValidationError import asdf diff --git a/dkist/io/asdf/converters/file_manager.py b/dkist/io/asdf/converters/file_manager.py index cfc68b53..fd2856dc 100644 --- a/dkist/io/asdf/converters/file_manager.py +++ b/dkist/io/asdf/converters/file_manager.py @@ -28,14 +28,17 @@ def from_yaml_tree(self, node, tag, ctx): node["target"], node["datatype"], node["shape"], + chunksize=node.get("chunksize", None), loader=AstropyFITSLoader, basepath=base_path) return file_manager def to_yaml_tree(self, obj, tag, ctx): node = {} - node["fileuris"] = obj._striped_external_array._fileuris.tolist() + node["fileuris"] = obj._striped_external_array.fileuri_array.tolist() node["target"] = obj._striped_external_array.target node["datatype"] = obj._striped_external_array.dtype node["shape"] = obj._striped_external_array.shape + if chunksize := obj._striped_external_array.chunksize is not None: + node["chunksize"] = chunksize return node diff --git a/dkist/io/asdf/entry_points.py b/dkist/io/asdf/entry_points.py index b90f6eb0..17e8a67b 100644 --- a/dkist/io/asdf/entry_points.py +++ b/dkist/io/asdf/entry_points.py @@ -1,7 +1,7 @@ """ This file contains the entry points for asdf. """ -import sys +import importlib.resources as importlib_resources from asdf.extension import ManifestExtension from asdf.resource import DirectoryResourceMapping @@ -10,12 +10,6 @@ FileManagerConverter, RavelConverter, TiledDatasetConverter, VaryingCelestialConverter) -if sys.version_info < (3, 9): - import importlib_resources -else: - import importlib.resources as importlib_resources - - def get_resource_mappings(): """ diff --git a/dkist/io/asdf/resources/schemas/file_manager-1.0.0.yaml b/dkist/io/asdf/resources/schemas/file_manager-1.0.0.yaml index 9d9698e6..335e9dc7 100644 --- a/dkist/io/asdf/resources/schemas/file_manager-1.0.0.yaml +++ b/dkist/io/asdf/resources/schemas/file_manager-1.0.0.yaml @@ -22,6 +22,8 @@ properties: anyOf: - type: integer minimum: 0 + chunksize: + type: array required: [fileuris, target, datatype, shape] additionalProperties: false diff --git a/dkist/io/asdf/tests/test_dataset.py b/dkist/io/asdf/tests/test_dataset.py index 35cf4b19..2a880a2f 100644 --- a/dkist/io/asdf/tests/test_dataset.py +++ b/dkist/io/asdf/tests/test_dataset.py @@ -1,11 +1,6 @@ -import sys +import importlib.resources as importlib_resources from pathlib import Path -if sys.version_info < (3, 9): - import importlib_resources -else: - import importlib.resources as importlib_resources - import numpy as np import pytest @@ -49,8 +44,8 @@ def assert_dataset_equal(new, old): new.meta["headers"] = new_headers assert old.wcs.name == new.wcs.name assert len(old.wcs.available_frames) == len(new.wcs.available_frames) - ac_new = new.files.external_array_references - ac_old = old.files.external_array_references + ac_new = new.files.fileuri_array + ac_old = old.files.fileuri_array assert ac_new == ac_old assert old.unit == new.unit assert old.mask == new.mask @@ -140,3 +135,39 @@ def test_read_all_schema_versions(eit_dataset_asdf_path): assert isinstance(dataset.wcs, gwcs.WCS) assert dataset.wcs.world_n_dim == 3 assert dataset.wcs.pixel_n_dim == 3 + + +@pytest.fixture +def wrap_object(mocker): + + def wrap_object(target, attribute): + mock = mocker.MagicMock() + real_attribute = getattr(target, attribute) + + def mocked_attribute(self, *args, **kwargs): + mock.__call__(*args, **kwargs) + return real_attribute(self, *args, **kwargs) + + mocker.patch.object(target, attribute, mocked_attribute) + + return mock + + return wrap_object + + +def test_loader_getitem_with_chunksize(eit_dataset_asdf_path, wrap_object): + # Import this here to prevent hitting https://bugs.python.org/issue35753 on Python <3.10 + # Importing call is enough to trigger a doctest error + from unittest.mock import call + + chunksize = (32, 16) + with asdf.open(eit_dataset_asdf_path) as tree: + dataset = tree["dataset"] + dataset.files.basepath = rootdir / "EIT" + dataset.files._striped_external_array.chunksize = chunksize + mocked = wrap_object(dataset.files._striped_external_array._loader, "__getitem__") + dataset._data = dataset.files._generate_array() + dataset.data.compute() + + expected_call = call((slice(0, chunksize[0], None), slice(0, chunksize[1], None))) + assert expected_call in mocked.mock_calls diff --git a/dkist/io/asdf/tests/test_tiled_dataset.py b/dkist/io/asdf/tests/test_tiled_dataset.py index 19038c48..8c806222 100644 --- a/dkist/io/asdf/tests/test_tiled_dataset.py +++ b/dkist/io/asdf/tests/test_tiled_dataset.py @@ -1,9 +1,4 @@ -import sys - -if sys.version_info < (3, 9): - import importlib_resources -else: - import importlib.resources as importlib_resources +import importlib.resources as importlib_resources import asdf diff --git a/dkist/io/dask_utils.py b/dkist/io/dask_utils.py index 6bbad5a0..feb8a3fb 100644 --- a/dkist/io/dask_utils.py +++ b/dkist/io/dask_utils.py @@ -6,7 +6,7 @@ __all__ = ['stack_loader_array'] -def stack_loader_array(loader_array): +def stack_loader_array(loader_array, chunksize): """ Stack a loader array along each of its dimensions. @@ -20,15 +20,18 @@ def stack_loader_array(loader_array): ------- array : `dask.array.Array` """ + # If the chunksize sin't specified then use the whole array shape + chunksize = chunksize or loader_array.flat[0].shape + if len(loader_array.shape) == 1: - return da.stack(loader_to_dask(loader_array)) + return da.stack(loader_to_dask(loader_array, chunksize)) stacks = [] for i in range(loader_array.shape[0]): - stacks.append(stack_loader_array(loader_array[i])) + stacks.append(stack_loader_array(loader_array[i], chunksize)) return da.stack(stacks) -def loader_to_dask(loader_array): +def loader_to_dask(loader_array, chunksize): """ Map a call to `dask.array.from_array` onto all the elements in ``loader_array``. @@ -44,6 +47,6 @@ def loader_to_dask(loader_array): # trying to auto calculate it by reading from the actual array on disk. meta = np.zeros((0,), dtype=loader_array[0].dtype) - to_array = partial(da.from_array, meta=meta) + to_array = partial(da.from_array, meta=meta, chunks=chunksize) return map(to_array, loader_array) diff --git a/dkist/io/file_manager.py b/dkist/io/file_manager.py index 0e6d08fd..a647a7c0 100644 --- a/dkist/io/file_manager.py +++ b/dkist/io/file_manager.py @@ -15,7 +15,7 @@ ``StripedExternalArrayView`` class. """ import os -from typing import Any, List, Tuple, Union, Iterable, Optional +from typing import Any, Tuple, Union, Iterable, Optional from pathlib import Path import dask.array @@ -27,7 +27,6 @@ except ImportError: NDArray = DTypeLike = Iterable -from asdf.tags.core.external_reference import ExternalArrayReference from astropy.wcs.wcsapi.wrappers.sliced_wcs import sanitize_slices from dkist.io.dask_utils import stack_loader_array @@ -43,10 +42,10 @@ class BaseStripedExternalArray: """ def __len__(self) -> int: - return self.reference_array.size + return self.loader_array.size def __eq__(self, other) -> bool: - uri = (self.reference_array == other.reference_array).all() + uri = (self.fileuri_array == other.fileuri_array).all() target = self.target == other.target dtype = self.dtype == other.dtype shape = self.shape == other.shape @@ -54,29 +53,22 @@ def __eq__(self, other) -> bool: return all((uri, target, dtype, shape)) @staticmethod - def _output_shape_from_ref_array(shape, reference_array) -> Tuple[int]: + def _output_shape_from_ref_array(shape, loader_array) -> Tuple[int]: # If the first dimension is one we are going to squash it. if shape[0] == 1: shape = shape[1:] - if len(reference_array) == 1: + if len(loader_array) == 1: return shape else: - return tuple(list(reference_array.shape) + list(shape)) + return tuple(list(loader_array.shape) + list(shape)) @property def output_shape(self) -> Tuple[int, ...]: """ The final shape of the reconstructed data array. """ - return self._output_shape_from_ref_array(self.shape, self.reference_array) - - @property - def _fileuris(self) -> NDArray[str]: - """ - Numpy array of fileuris - """ - return np.vectorize(lambda x: x.fileuri)(self.reference_array) + return self._output_shape_from_ref_array(self.shape, self.loader_array) def _generate_array(self) -> dask.array.Array: """ @@ -86,7 +78,7 @@ def _generate_array(self) -> dask.array.Array: still have a reference to this `~.FileManager` object, meaning changes to this object will be reflected in the data loaded by the array. """ - return stack_loader_array(self.loader_array).reshape(self.output_shape) + return stack_loader_array(self.loader_array, self.chunksize).reshape(self.output_shape) class StripedExternalArray(BaseStripedExternalArray): @@ -99,6 +91,7 @@ def __init__( *, loader: BaseFITSLoader, basepath: os.PathLike = None, + chunksize: Iterable[int] = None, ): shape = tuple(shape) self.shape = shape @@ -107,14 +100,13 @@ def __init__( self._loader = loader self._basepath = None self.basepath = basepath # Use the setter to convert to a Path + self.chunksize = chunksize - self._reference_array = np.vectorize( - lambda uri: ExternalArrayReference(uri, self.target, self.dtype, self.shape) - )(fileuris) + self._fileuri_array = np.array(fileuris) - loader_array = np.empty_like(self.reference_array, dtype=object) - for i, ele in enumerate(self.reference_array.flat): - loader_array.flat[i] = loader(ele, self) + loader_array = np.empty_like(self._fileuri_array, dtype=object) + for i, fileuri in enumerate(self._fileuri_array.flat): + loader_array.flat[i] = loader(fileuri, shape, dtype, target, self) self._loader_array = loader_array @@ -124,6 +116,10 @@ def __str__(self) -> str: def __repr__(self) -> str: return f"{object.__repr__(self)}\n{self}" + @property + def ndim(self): + return len(self.loader_array.shape) + @property def basepath(self) -> os.PathLike: """ @@ -136,11 +132,11 @@ def basepath(self, value: Optional[Union[os.PathLike, str]]): self._basepath = Path(value).expanduser() if value is not None else None @property - def reference_array(self) -> NDArray[ExternalArrayReference]: + def fileuri_array(self) -> NDArray[str]: """ - An array of `asdf.ExternalArrayReference` objects. + An array of relative (to ``basepath``) file uris. """ - return self._reference_array + return self._fileuri_array @property def loader_array(self) -> NDArray[BaseFITSLoader]: @@ -152,16 +148,10 @@ def loader_array(self) -> NDArray[BaseFITSLoader]: """ return self._loader_array - def _to_ears(self, urilist) -> List[ExternalArrayReference]: - # This is separate to the property because it's recursive - if isinstance(urilist, (list, tuple)): - return list(map(self._to_ears, urilist)) - return ExternalArrayReference(urilist, self.target, self.dtype, self.shape) - class StripedExternalArrayView(BaseStripedExternalArray): # This class presents a view int a FITSLoader object It applies a slice to - # the reference_array and loader_array properties Any property which + # the fileuri_array and loader_array properties Any property which # references the sliced objects should be defined in Base or this view # class. __slots__ = ["parent", "parent_slice"] @@ -191,11 +181,13 @@ def basepath(self, value): self.parent.basepath = value @property - def reference_array(self) -> NDArray[ExternalArrayReference]: + def fileuri_array(self) -> NDArray[str]: """ - An array of `asdf.ExternalArrayReference` objects. + An array of relative (to ``basepath``) file uris. """ - return np.array(self._reference_array[self.parent_slice]) + # array call here to ensure that a length one array is returned rather + # than a single element. + return np.array(self._fileuri_array[self.parent_slice]) @property def loader_array(self) -> NDArray[BaseFITSLoader]: @@ -205,14 +197,16 @@ def loader_array(self) -> NDArray[BaseFITSLoader]: These loader objects implement the minimal array-like interface for conversion to a dask array. """ + # array call here to ensure that a length one array is returned rather + # than a single element. return np.array(self._loader_array[self.parent_slice]) class BaseFileManager: @classmethod - def from_parts(cls, fileuris, target, dtype, shape, *, loader, basepath=None): + def from_parts(cls, fileuris, target, dtype, shape, *, loader, basepath=None, chunksize=None): fits_loader = StripedExternalArray( - fileuris, target, dtype, shape, loader=loader, basepath=basepath + fileuris, target, dtype, shape, loader=loader, basepath=basepath, chunksize=None, ) return cls(fits_loader) @@ -232,12 +226,12 @@ def __repr__(self) -> str: return f"{object.__repr__(self)}\n{self}" def __getitem__(self, item): - item = sanitize_slices(item, self._striped_external_array.reference_array.ndim) + item = sanitize_slices(item, self._striped_external_array.ndim) return type(self)(StripedExternalArrayView(self._striped_external_array, item)) - def _array_slice_to_reference_slice(self, aslice): + def _array_slice_to_loader_slice(self, aslice): """ - Convert a slice for the reconstructed array to a slice for the reference_array. + Convert a slice for the reconstructed array to a slice for the loader_array. """ fits_array_shape = self._striped_external_array.shape aslice = list(sanitize_slices(aslice, len(self.output_shape))) @@ -249,7 +243,7 @@ def _array_slice_to_reference_slice(self, aslice): return tuple(aslice) def _slice_by_cube(self, item): - item = self._array_slice_to_reference_slice(item) + item = self._array_slice_to_loader_slice(item) loader_view = StripedExternalArrayView(self._striped_external_array, item) return type(self)(loader_view) @@ -257,11 +251,13 @@ def _generate_array(self): return self._striped_external_array._generate_array() @property - def external_array_references(self): + def fileuri_array(self): """ - Represent this collection as a list of `asdf.ExternalArrayReference` objects. + An array of all the fileuris referenced by this `.FileManager`. + + This array is shaped to match the striped dimensions of the dataset. """ - return self._striped_external_array.reference_array.tolist() + return self._striped_external_array.fileuri_array @property def basepath(self): @@ -279,7 +275,7 @@ def filenames(self): """ Return a list of file names referenced by this Array Container. """ - return self._striped_external_array._fileuris.flatten().tolist() + return self.fileuri_array.flatten().tolist() @property def output_shape(self): diff --git a/dkist/io/loaders.py b/dkist/io/loaders.py index 2e607a90..32a3ac77 100644 --- a/dkist/io/loaders.py +++ b/dkist/io/loaders.py @@ -4,6 +4,7 @@ """ import abc +import logging from pathlib import Path import dask.array as da @@ -13,6 +14,8 @@ from astropy.io import fits from sunpy.util.decorators import add_common_docstring +_LOGGER = logging.getLogger(__name__) + __all__ = ['BaseFITSLoader', 'AstropyFITSLoader'] @@ -20,41 +23,52 @@ Parameters ---------- - - externalarray: `asdf.ExternalArrayReference` - The asdf array reference, must be to a FITS file (although this is not validated). - - basepath: `str` - The base path for the filenames in the `asdf.ExternalArrayReference`, - if not specified the filepaths are treated as absolute. + fileuri: `str` + The filename, either absolute, or if `basepath` is specified, relative to `basepath`. + shape: `tuple` + The shape of the array to be proxied. + dtype: `numpy.dtype` + The dtype of the resulting array + target: `int` + The HDU number to load the array from. + array_container: `BaseStripedExternalArray` + The parent object of this class, which builds the array from a sequence + of these loaders. """ @add_common_docstring(append=common_parameters) class BaseFITSLoader(metaclass=abc.ABCMeta): """ - Base class for resolving an `asdf.ExternalArrayReference` to a FITS file. + Base FITS array proxy. + + This class implements the array-like API needed for dask to convert a FITS + array into a dask array without loading the FITS file until data access + time. """ - def __init__(self, externalarray, array_container): - self.externalarray = externalarray + def __init__(self, fileuri, shape, dtype, target, array_container): + self.fileuri = fileuri + self.shape = shape + self.dtype = dtype + self.target = target self.array_container = array_container - # These are needed for this object to be array-like - self.shape = self.externalarray.shape self.ndim = len(self.shape) - self.dtype = self.externalarray.dtype + self.size = np.prod(self.shape) def __repr__(self): return self.__str__() def __str__(self): - return "".format(self.externalarray) + return "".format(self) - def __array__(self): - return self.fits_array + @property + def data(self): + return self[:] + @abc.abstractmethod def __getitem__(self, slc): - return self.fits_array[slc] + pass @property def basepath(self): @@ -66,22 +80,9 @@ def absolute_uri(self): Construct a non-relative path to the file, using ``basepath`` if provided. """ if self.basepath: - return self.basepath / self.externalarray.fileuri + return self.basepath / self.fileuri else: - return Path(self.externalarray.fileuri) - - @property - def fits_array(self): - """ - The FITS array object. - """ - return self._read_fits_array() - - @abc.abstractmethod - def _read_fits_array(self): - """ - Read and return a reference to the FITS array. - """ + return Path(self.fileuri) @add_common_docstring(append=common_parameters) @@ -90,7 +91,7 @@ class AstropyFITSLoader(BaseFITSLoader): Resolve an `~asdf.ExternalArrayReference` to a FITS file using `astropy.io.fits`. """ - def _read_fits_array(self): + def __getitem__(self, slc): if not self.absolute_uri.exists(): # Use np.broadcast_to to generate an array of the correct size, but # which only uses memory for one value. @@ -98,10 +99,13 @@ def _read_fits_array(self): self.shape, self.dtype) with fits.open(self.absolute_uri, - memmap=True, # don't load the whole array into memory, let dask access the part it needs - do_not_scale_image_data=True, # don't scale as that would cause it to be loaded into memory + memmap=False, # memmap is redundant with dask and delayed loading + do_not_scale_image_data=True, # don't scale as we shouldn't need to mode="denywrite") as hdul: + _LOGGER.debug("Accessing slice %s from file %s", slc, self.absolute_uri) - hdul.verify('fix') - hdu = hdul[self.externalarray.target] - return hdu.data + hdu = hdul[self.target] + if hasattr(hdu, "section"): + return hdu.section[slc] + else: + return hdu.data[slc] diff --git a/dkist/io/tests/test_file_manager.py b/dkist/io/tests/test_file_manager.py index 25463350..e21fb6c7 100644 --- a/dkist/io/tests/test_file_manager.py +++ b/dkist/io/tests/test_file_manager.py @@ -5,8 +5,6 @@ import pytest from numpy.testing import assert_allclose -import asdf - from dkist import net from dkist.data.test import rootdir from dkist.io.file_manager import FileManager, StripedExternalArray, StripedExternalArrayView @@ -23,15 +21,15 @@ def file_manager(eit_dataset): @pytest.fixture -def externalarray(file_manager): +def loader_array(file_manager): """ An array of external array references. """ - return file_manager.external_array_references + return file_manager._striped_external_array.loader_array -def test_load_and_slicing(file_manager, externalarray): - ext_shape = np.array(externalarray, dtype=object).shape +def test_load_and_slicing(file_manager, loader_array): + ext_shape = np.array(loader_array, dtype=object).shape assert file_manager._striped_external_array.loader_array.shape == ext_shape assert file_manager.output_shape == tuple(list(ext_shape) + [128, 128]) @@ -41,18 +39,18 @@ def test_load_and_slicing(file_manager, externalarray): assert not np.isnan(array).all() sliced_manager = file_manager[5:8] - ext_shape = np.array(externalarray[5:8], dtype=object).shape + ext_shape = np.array(loader_array[5:8], dtype=object).shape assert sliced_manager._striped_external_array.loader_array.shape == ext_shape assert sliced_manager.output_shape == tuple(list(ext_shape) + [128, 128]) -def test_filenames(file_manager, externalarray): - assert len(file_manager.filenames) == len(externalarray) - assert file_manager.filenames == [e.fileuri for e in externalarray] +def test_filenames(file_manager, loader_array): + assert len(file_manager.filenames) == len(loader_array) + assert (file_manager.filenames == file_manager._striped_external_array.fileuri_array.flatten()).all() -def test_dask(file_manager, externalarray): - ext_shape = np.array(externalarray, dtype=object).shape +def test_dask(file_manager, loader_array): + ext_shape = np.array(loader_array, dtype=object).shape assert file_manager._striped_external_array.loader_array.shape == ext_shape assert file_manager.output_shape == tuple(list(ext_shape) + [128, 128]) @@ -60,16 +58,6 @@ def test_dask(file_manager, externalarray): assert_allclose(file_manager._generate_array(), np.array(file_manager._generate_array())) -def test_collection_to_references(tmpdir, file_manager): - ears = file_manager.external_array_references - - for ear in ears: - assert isinstance(ear, asdf.ExternalArrayReference) - assert ear.target == file_manager._striped_external_array.target - assert ear.dtype == file_manager._striped_external_array.dtype - assert ear.shape == file_manager._striped_external_array.shape - - def test_collection_getitem(tmpdir, file_manager): assert isinstance(file_manager._striped_external_array, StripedExternalArray) assert isinstance(file_manager[0], FileManager) diff --git a/dkist/io/tests/test_fits.py b/dkist/io/tests/test_fits.py index e5b6231f..a2540a5e 100644 --- a/dkist/io/tests/test_fits.py +++ b/dkist/io/tests/test_fits.py @@ -70,7 +70,7 @@ def test_construct(relative_fl, absolute_fl): def test_array(absolute_fl): - a = absolute_fl.fits_array + a = absolute_fl.data assert isinstance(a, np.ndarray) for contain in ("efz20040301.000010_s.fits", str(absolute_fl.shape), absolute_fl.dtype): @@ -83,14 +83,8 @@ def test_nan(relative_ac, tmpdir): array = relative_ac._generate_array() assert_allclose(array[10:20, :], np.nan) - -def test_np_array(absolute_fl): - narr = np.array(absolute_fl) - assert_allclose(narr, absolute_fl.fits_array) - assert narr is not absolute_fl.fits_array - def test_slicing(absolute_fl): aslice = np.s_[10:20, 10:20] sarr = absolute_fl[aslice] - assert_allclose(sarr, absolute_fl.fits_array[aslice]) + assert_allclose(sarr, absolute_fl.data[aslice]) diff --git a/setup.cfg b/setup.cfg index 8a98ee45..958a2be1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,13 +12,12 @@ github_project = DKISTDC/dkist classifiers = Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 [options] -python_requires = >=3.8 +python_requires = >=3.9 packages = find: include_package_data = True install_requires = @@ -30,12 +29,11 @@ install_requires = dask[array]>=2 globus-sdk>=3.0 gwcs>=0.18.0 - matplotlib>=3.1 + matplotlib>=3.4 ndcube[plotting,reproject]>=2.0 numpy>=1.21 parfive[ftp]>=1.3 sunpy[net,asdf]>=4 - importlib-resources; python_version<"3.9" setup_requires = setuptools_scm [options.extras_require] diff --git a/tox.ini b/tox.ini index ef425505..f093024b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{38,39,310}{,-devdeps,-oldestdeps},build_docs,codestyle +envlist = py{39,310,311}{,-devdeps,-oldestdeps},build_docs,codestyle requires = setuptools >= 30.3.0 pip >= 21.0.1 @@ -40,7 +40,7 @@ deps = oldestdeps: dask[array]<2.1 oldestdeps: globus-sdk<3.1 oldestdeps: gwcs<0.19 - oldestdeps: matplotlib<3.3 + oldestdeps: matplotlib<3.5 oldestdeps: ndcube<2.1 oldestdeps: numpy<1.22 oldestdeps: parfive[ftp]<1.3