diff --git a/.ruff.toml b/.ruff.toml index 00bde5c58..31e599f95 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -62,6 +62,10 @@ extend-ignore = [ "docs/*.py" = [ "INP001", # Implicit-namespace-package. The examples are not a package. ] +"examples/*.py" = [ + "T201", + # We need print in our examples +] "__init__.py" = ["E402", "F401", "F403"] "test_*.py" = ["B011", "D", "E402", "PGH001", "S101"] diff --git a/docs/conf.py b/docs/conf.py index 30b2b3b05..34b8fdfb6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,7 +58,7 @@ ] # Add any paths that contain templates here, relative to this directory. -# templates_path = ["_templates"] # NOQA: ERA001 +# templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -116,7 +116,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ["_static"] # NOQA: ERA001 +# html_static_path = ["_static"] # By default, when rendering docstrings for classes, sphinx.ext.autodoc will # make docs with the class-level docstring and the class-method docstrings, diff --git a/examples/creating_even_spaced_wavelength_visualisation.py b/examples/creating_even_spaced_wavelength_visualisation.py index 8ea953828..995fdd545 100644 --- a/examples/creating_even_spaced_wavelength_visualisation.py +++ b/examples/creating_even_spaced_wavelength_visualisation.py @@ -34,7 +34,7 @@ # `sequence=True` causes a sequence of maps to be returned, one for each image file. sequence_of_maps = sunpy.map.Map(aia_files, sequence=True) # Sort the maps in the sequence in order of wavelength. -sequence_of_maps.maps = list(sorted(sequence_of_maps.maps, key=lambda m: m.wavelength)) +sequence_of_maps.maps = sorted(sequence_of_maps.maps, key=lambda m: m.wavelength) ############################################################################# # Using an `astropy.units.Quantity` of the wavelengths of the images, we can construct diff --git a/ndcube/extra_coords/extra_coords.py b/ndcube/extra_coords/extra_coords.py index 87f4de9b7..d937eac39 100644 --- a/ndcube/extra_coords/extra_coords.py +++ b/ndcube/extra_coords/extra_coords.py @@ -230,8 +230,8 @@ def add(self, name, array_dimension, lookup_table, physical_types=None, **kwargs self._lookup_tables.append((array_dimension, coord)) # Sort the LUTs so that the mapping and the wcs are ordered in pixel dim order - self._lookup_tables = list(sorted(self._lookup_tables, - key=lambda x: x[0] if isinstance(x[0], Integral) else x[0][0])) + self._lookup_tables = sorted(self._lookup_tables, + key=lambda x: x[0] if isinstance(x[0], Integral) else x[0][0]) @property def _name_lut_map(self): @@ -323,8 +323,7 @@ def is_empty(self): # docstring in ABC if not self._wcs and not self._lookup_tables: return True - else: - return False + return False def _getitem_string(self, item): """ @@ -402,7 +401,7 @@ def __getitem__(self, item): if self._wcs: return self._getitem_wcs(item) - elif self._lookup_tables: + if self._lookup_tables: return self._getitem_lookup_tables(item) # If we get here this object is empty, so just return an empty extra coords diff --git a/ndcube/extra_coords/table_coord.py b/ndcube/extra_coords/table_coord.py index 503d6f1c7..e38eb2844 100644 --- a/ndcube/extra_coords/table_coord.py +++ b/ndcube/extra_coords/table_coord.py @@ -225,8 +225,7 @@ def __str__(self): content = str(self.table).lstrip('(').rstrip(',)') if len(header) + len(content) >= np.get_printoptions()['linewidth']: return '\n'.join((header, content)) - else: - return ' '.join((header, content)) + return ' '.join((header, content)) def __repr__(self): return f"{object.__repr__(self)}\n{self}" @@ -539,16 +538,15 @@ def __getitem__(self, item): mesh=False, names=self.names, physical_types=self.physical_types) - else: - self._slice = [self.combine_slices(a, b) for a, b in zip(sane_item, self._slice)] - if all([isinstance(s, Integral) for s in self._slice]): - # Here we rebuild the SkyCoord with the slice applied to the individual components. - new_sc = SkyCoord(self.table.realize_frame(type(self.table.data)(*self._sliced_components))) - return type(self)(new_sc, - mesh=False, - names=self.names, - physical_types=self.physical_types) - return self + self._slice = [self.combine_slices(a, b) for a, b in zip(sane_item, self._slice)] + if all([isinstance(s, Integral) for s in self._slice]): + # Here we rebuild the SkyCoord with the slice applied to the individual components. + new_sc = SkyCoord(self.table.realize_frame(type(self.table.data)(*self._sliced_components))) + return type(self)(new_sc, + mesh=False, + names=self.names, + physical_types=self.physical_types) + return self @property def frame(self): @@ -590,8 +588,7 @@ def ndim(self): """ if self.mesh: return len(self.table.data.components) - else: - return self.table.ndim + return self.table.ndim @property def shape(self): @@ -605,8 +602,7 @@ def shape(self): """ if self.mesh: return tuple(list(self.table.shape) * self.ndim) - else: - return self.table.shape + return self.table.shape def interpolate(self, *new_array_grids, mesh_output=None, **kwargs): """ @@ -892,19 +888,18 @@ def frame(self): """ if len(self._table_coords) == 1: return self._table_coords[0].frame - else: - frames = [t.frame for t in self._table_coords] - - # We now have to set the axes_order of all the frames so that we - # have one consistent WCS with the correct number of pixel - # dimensions. - ind = 0 - for f in frames: - new_ind = ind + f.naxes - f._axes_order = tuple(range(ind, new_ind)) - ind = new_ind - - return cf.CompositeFrame(frames) + frames = [t.frame for t in self._table_coords] + + # We now have to set the axes_order of all the frames so that we + # have one consistent WCS with the correct number of pixel + # dimensions. + ind = 0 + for f in frames: + new_ind = ind + f.naxes + f._axes_order = tuple(range(ind, new_ind)) + ind = new_ind + + return cf.CompositeFrame(frames) @property def dropped_world_dimensions(self): diff --git a/ndcube/global_coords.py b/ndcube/global_coords.py index a15c1facf..35403f97e 100644 --- a/ndcube/global_coords.py +++ b/ndcube/global_coords.py @@ -232,7 +232,7 @@ def __len__(self): def __str__(self): classname = self.__class__.__name__ - elements = [f"{name} {[ptype]}:\n{repr(coord)}" for (name, coord), ptype in + elements = [f"{name} {[ptype]}:\n{coord!r}" for (name, coord), ptype in zip(self.items(), self.physical_types.values())] length = len(classname) + 2 * len(elements) + sum(len(e) for e in elements) if length > np.get_printoptions()['linewidth']: @@ -243,4 +243,4 @@ def __str__(self): return f"{classname}({joiner.join(elements)})" def __repr__(self): - return f"{object.__repr__(self)}\n{str(self)}" + return f"{object.__repr__(self)}\n{self!s}" diff --git a/ndcube/ndcollection.py b/ndcube/ndcollection.py index 0b37e4e91..3a4ae2c53 100644 --- a/ndcube/ndcollection.py +++ b/ndcube/ndcollection.py @@ -92,7 +92,7 @@ def __str__(self): Aligned physical types: {self.aligned_axis_physical_types}""")) def __repr__(self): - return f"{object.__repr__(self)}\n{str(self)}" + return f"{object.__repr__(self)}\n{self!s}" @property def aligned_dimensions(self): @@ -137,40 +137,39 @@ def __getitem__(self, item): return super().__getitem__(item) # If item is not a single string... + # If item is a sequence, ensure strings and numeric items are not mixed. + item_is_strings = False + if isinstance(item, collections.abc.Sequence): + item_strings = [isinstance(item_, str) for item_ in item] + item_is_strings = all(item_strings) + # Ensure strings are not mixed with slices. + if (not item_is_strings) and (not all(np.invert(item_strings))): + raise TypeError("Cannot mix keys and non-keys when indexing instance.") + + # If sequence is all strings, extract the cubes corresponding to the string keys. + if item_is_strings: + new_data = [self[_item] for _item in item] + new_keys = item + new_aligned_axes = tuple([self.aligned_axes[item_] for item_ in item]) + + # Else, the item is assumed to be a typical slicing item. + # Slice each cube in collection using information in this item. + # However, this can only be done if there are aligned axes. else: - # If item is a sequence, ensure strings and numeric items are not mixed. - item_is_strings = False - if isinstance(item, collections.abc.Sequence): - item_strings = [isinstance(item_, str) for item_ in item] - item_is_strings = all(item_strings) - # Ensure strings are not mixed with slices. - if (not item_is_strings) and (not all(np.invert(item_strings))): - raise TypeError("Cannot mix keys and non-keys when indexing instance.") - - # If sequence is all strings, extract the cubes corresponding to the string keys. - if item_is_strings: - new_data = [self[_item] for _item in item] - new_keys = item - new_aligned_axes = tuple([self.aligned_axes[item_] for item_ in item]) - - # Else, the item is assumed to be a typical slicing item. - # Slice each cube in collection using information in this item. - # However, this can only be done if there are aligned axes. - else: - if self.aligned_axes is None: - raise IndexError("Cannot slice unless collection has aligned axes.") - # Derive item to be applied to each cube in collection and - # whether any aligned axes are dropped by the slicing. - collection_items, new_aligned_axes = self._generate_collection_getitems(item) - # Apply those slice items to each cube in collection. - new_data = [self[key][tuple(cube_item)] - for key, cube_item in zip(self, collection_items)] - # Since item is not strings, no cube in collection is dropped. - # Therefore the collection keys remain unchanged. - new_keys = list(self.keys()) - - return self.__class__(list(zip(new_keys, new_data)), aligned_axes=new_aligned_axes, - meta=self.meta, sanitize_inputs=False) + if self.aligned_axes is None: + raise IndexError("Cannot slice unless collection has aligned axes.") + # Derive item to be applied to each cube in collection and + # whether any aligned axes are dropped by the slicing. + collection_items, new_aligned_axes = self._generate_collection_getitems(item) + # Apply those slice items to each cube in collection. + new_data = [self[key][tuple(cube_item)] + for key, cube_item in zip(self, collection_items)] + # Since item is not strings, no cube in collection is dropped. + # Therefore the collection keys remain unchanged. + new_keys = list(self.keys()) + + return self.__class__(list(zip(new_keys, new_data)), aligned_axes=new_aligned_axes, + meta=self.meta, sanitize_inputs=False) def _generate_collection_getitems(self, item): # There are 3 supported cases of the slice item: int, slice, tuple of ints and/or slices. diff --git a/ndcube/ndcube.py b/ndcube/ndcube.py index e7cec0364..11e56a493 100644 --- a/ndcube/ndcube.py +++ b/ndcube/ndcube.py @@ -309,7 +309,7 @@ def __set_name__(self, owner, name): def __get__(self, obj, objtype=None): if obj is None: - return + return None if getattr(obj, self._attribute_name, None) is None and self._default_type is not None: self.__set__(obj, self._default_type) @@ -580,24 +580,23 @@ def _get_crop_item(self, *points, wcs=None, keepdims=False): # Quit out early if we are no-op if no_op: return tuple([slice(None)] * wcs.pixel_n_dim) - else: - comp = [c[0] for c in wcs.world_axis_object_components] - # Trim to unique component names - `np.unique(..., return_index=True) - # keeps sorting alphabetically, set() seems just nondeterministic. - for k, c in enumerate(comp): - if comp.count(c) > 1: - comp.pop(k) - classes = [wcs.world_axis_object_classes[c][0] for c in comp] - for i, point in enumerate(points): - if len(point) != len(comp): - raise ValueError(f"{len(point)} components in point {i} do not match " - f"WCS with {len(comp)} components.") - for j, value in enumerate(point): - if not (value is None or isinstance(value, classes[j])): - raise TypeError(f"{type(value)} of component {j} in point {i} is " - f"incompatible with WCS component {comp[j]} " - f"{classes[j]}.") - return utils.cube.get_crop_item_from_points(points, wcs, False, keepdims=keepdims) + comp = [c[0] for c in wcs.world_axis_object_components] + # Trim to unique component names - `np.unique(..., return_index=True) + # keeps sorting alphabetically, set() seems just nondeterministic. + for k, c in enumerate(comp): + if comp.count(c) > 1: + comp.pop(k) + classes = [wcs.world_axis_object_classes[c][0] for c in comp] + for i, point in enumerate(points): + if len(point) != len(comp): + raise ValueError(f"{len(point)} components in point {i} do not match " + f"WCS with {len(comp)} components.") + for j, value in enumerate(point): + if not (value is None or isinstance(value, classes[j])): + raise TypeError(f"{type(value)} of component {j} in point {i} is " + f"incompatible with WCS component {comp[j]} " + f"{classes[j]}.") + return utils.cube.get_crop_item_from_points(points, wcs, False, keepdims=keepdims) def crop_by_values(self, *points, units=None, wcs=None, keepdims=False): # The docstring is defined in NDCubeABC @@ -651,7 +650,7 @@ def __str__(self): Data Type: {self.data.dtype}""") def __repr__(self): - return f"{object.__repr__(self)}\n{str(self)}" + return f"{object.__repr__(self)}\n{self!s}" def explode_along_axis(self, axis): """ @@ -866,14 +865,13 @@ class NDCube(NDCubeBase): def _as_mpl_axes(self): if hasattr(self.plotter, "_as_mpl_axes"): return self.plotter._as_mpl_axes() - else: - warn_user(f"The current plotter {self.plotter} does not have a '_as_mpl_axes' method. " - "The default MatplotlibPlotter._as_mpl_axes method will be used instead.") + warn_user(f"The current plotter {self.plotter} does not have a '_as_mpl_axes' method. " + "The default MatplotlibPlotter._as_mpl_axes method will be used instead.") - from ndcube.visualization.mpl_plotter import MatplotlibPlotter + from ndcube.visualization.mpl_plotter import MatplotlibPlotter - plotter = MatplotlibPlotter(self) - return plotter._as_mpl_axes() + plotter = MatplotlibPlotter(self) + return plotter._as_mpl_axes() def plot(self, *args, **kwargs): """ @@ -1268,11 +1266,10 @@ def _create_masked_array_for_rebinning(data, mask, operation_ignores_mask): m = None if (mask is None or mask is False or operation_ignores_mask) else mask if m is None: return data, m + for array_type, masked_type in ARRAY_MASK_MAP.items(): + if isinstance(data, array_type): + break else: - for array_type, masked_type in ARRAY_MASK_MAP.items(): - if isinstance(data, array_type): - break - else: - masked_type = np.ma.masked_array - warn_user("data and mask arrays of different or unrecognized types. Casting them into a numpy masked array.") - return masked_type(data, m), m + masked_type = np.ma.masked_array + warn_user("data and mask arrays of different or unrecognized types. Casting them into a numpy masked array.") + return masked_type(data, m), m diff --git a/ndcube/ndcube_sequence.py b/ndcube/ndcube_sequence.py index 35d541c7c..5e14d48d3 100644 --- a/ndcube/ndcube_sequence.py +++ b/ndcube/ndcube_sequence.py @@ -396,7 +396,7 @@ def __str__(self): Common Cube Axis: {self._common_axis}""")) def __repr__(self): - return f"{object.__repr__(self)}\n{str(self)}" + return f"{object.__repr__(self)}\n{self!s}" def __len__(self): return len(self.data) @@ -510,25 +510,24 @@ def __getitem__(self, item): cube_item = copy.deepcopy(item) cube_item[common_axis] = common_axis_index return self.seq.data[sequence_index][tuple(cube_item)] - else: - # item can now only be a tuple whose common axis item is a non-None slice object. - # Convert item into iterable of SequenceItems and slice each cube appropriately. - # item for common_axis must always be a slice for every cube, - # even if it is only a length-1 slice. - # Thus NDCubeSequence.index_as_cube can only slice away common axis if - # item is int or item's first item is an int. - # i.e. NDCubeSequence.index_as_cube cannot cause common_axis to become None - # since in all cases where the common_axis is sliced away involve an NDCube - # is returned, not an NDCubeSequence. - # common_axis of returned sequence must be altered if axes in front of it - # are sliced away. - sequence_items = utils.sequence.cube_like_tuple_item_to_sequence_items( - item, common_axis, common_axis_lengths, n_cube_dims) - # Work out new common axis value if axes in front of it are sliced away. - new_common_axis = common_axis - sum([isinstance(i, numbers.Integral) - for i in item[:common_axis]]) - # Copy sequence and alter the data and common axis. - result = type(self.seq)([], meta=self.seq.meta, common_axis=new_common_axis) - result.data = [self.seq.data[sequence_item.sequence_index][sequence_item.cube_item] - for sequence_item in sequence_items] - return result + # item can now only be a tuple whose common axis item is a non-None slice object. + # Convert item into iterable of SequenceItems and slice each cube appropriately. + # item for common_axis must always be a slice for every cube, + # even if it is only a length-1 slice. + # Thus NDCubeSequence.index_as_cube can only slice away common axis if + # item is int or item's first item is an int. + # i.e. NDCubeSequence.index_as_cube cannot cause common_axis to become None + # since in all cases where the common_axis is sliced away involve an NDCube + # is returned, not an NDCubeSequence. + # common_axis of returned sequence must be altered if axes in front of it + # are sliced away. + sequence_items = utils.sequence.cube_like_tuple_item_to_sequence_items( + item, common_axis, common_axis_lengths, n_cube_dims) + # Work out new common axis value if axes in front of it are sliced away. + new_common_axis = common_axis - sum([isinstance(i, numbers.Integral) + for i in item[:common_axis]]) + # Copy sequence and alter the data and common axis. + result = type(self.seq)([], meta=self.seq.meta, common_axis=new_common_axis) + result.data = [self.seq.data[sequence_item.sequence_index][sequence_item.cube_item] + for sequence_item in sequence_items] + return result diff --git a/ndcube/tests/helpers.py b/ndcube/tests/helpers.py index e82215db0..e3c1b8ef2 100644 --- a/ndcube/tests/helpers.py +++ b/ndcube/tests/helpers.py @@ -107,8 +107,7 @@ def assert_cubes_equal(test_input, expected_cube, check_data=True): assert np.all(test_input.shape == expected_cube.shape) assert_metas_equal(test_input.meta, expected_cube.meta) if type(test_input.extra_coords) is not type(expected_cube.extra_coords): - raise AssertionError("NDCube extra_coords not of same type: {0} != {1}".format( - type(test_input.extra_coords), type(expected_cube.extra_coords))) + raise AssertionError(f"NDCube extra_coords not of same type: {type(test_input.extra_coords)} != {type(expected_cube.extra_coords)}") if test_input.extra_coords is not None: assert_extra_coords_equal(test_input.extra_coords, expected_cube.extra_coords) diff --git a/ndcube/utils/collection.py b/ndcube/utils/collection.py index 452344c8d..29c1408c6 100644 --- a/ndcube/utils/collection.py +++ b/ndcube/utils/collection.py @@ -9,7 +9,7 @@ def _sanitize_aligned_axes(keys, data, aligned_axes): if aligned_axes is None: return None # If aligned_axes set to "all", assume all axes are aligned in order. - elif isinstance(aligned_axes, str) and aligned_axes.lower() == "all": + if isinstance(aligned_axes, str) and aligned_axes.lower() == "all": # Check all cubes are of same shape cube0_dims = data[0].shape cubes_same_shape = all([all([d.shape[i] == dim for i, dim in enumerate(cube0_dims)]) diff --git a/ndcube/utils/cube.py b/ndcube/utils/cube.py index af6485d24..290183bb0 100644 --- a/ndcube/utils/cube.py +++ b/ndcube/utils/cube.py @@ -257,7 +257,7 @@ def propagate_rebin_uncertainties(uncertainty, data, mask, operation, operation_ if operation in {np.sum, np.nansum, np.mean, np.nanmean}: propagation_operation = np.add # TODO: product was renamed to prod for numpy 2.0 - elif operation in {np.prod, np.nanprod, np.product if hasattr(np, "product") else np.prod}: + elif operation in {np.prod, np.nanprod, np.prod if hasattr(np, "product") else np.prod}: propagation_operation = np.multiply else: raise ValueError("propagation_operation not recognized.") diff --git a/ndcube/utils/wcs.py b/ndcube/utils/wcs.py index 9268676e2..7cace5276 100644 --- a/ndcube/utils/wcs.py +++ b/ndcube/utils/wcs.py @@ -450,10 +450,9 @@ def get_low_level_wcs(wcs, name='wcs'): if isinstance(wcs, BaseHighLevelWCS): return wcs.low_level_wcs - elif isinstance(wcs, BaseLowLevelWCS): + if isinstance(wcs, BaseLowLevelWCS): return wcs - else: - raise ValueError(f'{name} must implement either BaseHighLevelWCS or BaseLowLevelWCS') + raise ValueError(f'{name} must implement either BaseHighLevelWCS or BaseLowLevelWCS') def compare_wcs_physical_types(source_wcs, target_wcs): diff --git a/ndcube/visualization/descriptor.py b/ndcube/visualization/descriptor.py index 06926fe80..78fe3609a 100644 --- a/ndcube/visualization/descriptor.py +++ b/ndcube/visualization/descriptor.py @@ -53,16 +53,16 @@ def _resolve_default_type(self, raise_error=True): # If we have no default type then just return None else: - return + return None def __get__(self, obj, objtype=None): if obj is None: - return + return None if getattr(obj, self._attribute_name, None) is None: plotter_type = self._resolve_default_type() if plotter_type is None: - return + return None self.__set__(obj, plotter_type) diff --git a/ndcube/visualization/mpl_sequence_plotter.py b/ndcube/visualization/mpl_sequence_plotter.py index 5fff46939..1b1e477c3 100644 --- a/ndcube/visualization/mpl_sequence_plotter.py +++ b/ndcube/visualization/mpl_sequence_plotter.py @@ -32,8 +32,7 @@ def plot(self, sequence_axis_coords=None, sequence_axis_unit=None, **kwargs): sequence_dims = self._ndcube.shape if len(sequence_dims) == 2: raise NotImplementedError("Visualizing sequences of 1-D cubes not currently supported.") - else: - return self.animate(sequence_axis_coords, sequence_axis_unit, **kwargs) + return self.animate(sequence_axis_coords, sequence_axis_unit, **kwargs) def animate(self, sequence_axis_coords=None, sequence_axis_unit=None, **kwargs): """ diff --git a/ndcube/wcs/wrappers/__init__.py b/ndcube/wcs/wrappers/__init__.py index d040709cf..4fcb58fdd 100644 --- a/ndcube/wcs/wrappers/__init__.py +++ b/ndcube/wcs/wrappers/__init__.py @@ -1,3 +1,3 @@ -from .compound_wcs import * # NOQA -from .reordered_wcs import * # NOQA -from .resampled_wcs import * # NOQA +from .compound_wcs import * +from .reordered_wcs import * +from .resampled_wcs import *