diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 04689284..00414903 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,22 +3,18 @@ ci: repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 + rev: v3.19.1 hooks: - id: pyupgrade args: ["--py310-plus"] - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: 'v0.6.9' + rev: 'v0.9.3' hooks: - id: ruff - args: ["--show-fixes", "--fix"] - - - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.10.0 - hooks: - - id: black + args: ["--fix", "--show-fixes"] + - id: ruff-format - repo: https://github.com/rstcheck/rstcheck rev: v6.2.4 @@ -28,7 +24,7 @@ repos: args: ['--config', 'pyproject.toml'] - repo: https://github.com/executablebooks/mdformat - rev: 0.7.17 + rev: 0.7.21 hooks: - id: mdformat additional_dependencies: @@ -36,10 +32,8 @@ repos: - mdformat-myst - repo: https://github.com/nbQA-dev/nbQA - rev: 1.8.7 + rev: 1.9.1 hooks: - - id: nbqa-black - - id: nbqa-ruff - id: nbqa entry: nbqa mdformat name: nbqa-mdformat @@ -55,19 +49,13 @@ repos: - id: check-yaml - id: debug-statements - - repo: https://github.com/keewis/blackdoc - rev: v0.3.9 - hooks: - - id: blackdoc - files: .+\.py$ - - repo: https://github.com/citation-file-format/cff-converter-python rev: "44e8fc9" hooks: - id: validate-cff - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.20.2 + rev: v0.23 hooks: - id: validate-pyproject diff --git a/.readthedocs.yml b/.readthedocs.yml index 2d66a4bd..51b0a0a9 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,6 @@ version: 2 - +sphinx: + configuration: doc/conf.py build: os: ubuntu-lts-latest tools: diff --git a/cf_xarray/accessor.py b/cf_xarray/accessor.py index 36a65c34..2e9341de 100644 --- a/cf_xarray/accessor.py +++ b/cf_xarray/accessor.py @@ -680,7 +680,7 @@ def _getattr( None, ): raise AttributeError( - f"{obj.__class__.__name__+'.cf'!r} object has no attribute {attr!r}" + f"{obj.__class__.__name__ + '.cf'!r} object has no attribute {attr!r}" ) from None raise AttributeError( f"{attr!r} is not a valid attribute on the underlying xarray object." @@ -1374,7 +1374,11 @@ def curvefit( coords_iter = coords coords = [ apply_mapper( - [_single(_get_coords)], self._obj, v, error=False, default=[v] # type: ignore[arg-type] + [_single(_get_coords)], # type:ignore[arg-type] + self._obj, + v, + error=False, + default=[v], )[0] for v in coords_iter ] @@ -1385,7 +1389,11 @@ def curvefit( reduce_dims_iter = list(reduce_dims) reduce_dims = [ apply_mapper( - [_single(_get_dims)], self._obj, v, error=False, default=[v] # type: ignore[arg-type] + [_single(_get_dims)], # type:ignore[arg-type] + self._obj, + v, + error=False, + default=[v], )[0] for v in reduce_dims_iter ] @@ -2758,9 +2766,9 @@ def decode_vertical_coords(self, *, outnames=None, prefix=None): for dim in allterms: if prefix is None: - assert ( - outnames is not None - ), "if prefix is None, outnames must be provided" + assert outnames is not None, ( + "if prefix is None, outnames must be provided" + ) # set outnames here try: zname = outnames[dim] diff --git a/cf_xarray/datasets.py b/cf_xarray/datasets.py index 997ba987..32955f25 100644 --- a/cf_xarray/datasets.py +++ b/cf_xarray/datasets.py @@ -21,15 +21,14 @@ # POM dataset pomds = xr.Dataset() +# fmt: off pomds["sigma"] = ( - # fmt: off "sigma", [-0.983333, -0.95 , -0.916667, -0.883333, -0.85 , -0.816667, -0.783333, -0.75 , -0.716667, -0.683333, -0.65 , -0.616667, -0.583333, -0.55 , -0.516667, -0.483333, -0.45 , -0.416667, -0.383333, -0.35 , -0.316667, -0.283333, -0.25 , -0.216667, -0.183333, -0.15 , -0.116667, -0.083333, -0.05 , -0.016667], - # fmt: on { "units": "sigma_level", "long_name": "Sigma Stretched Vertical Coordinate at Nodes", @@ -38,6 +37,7 @@ "formula_terms": "sigma: sigma eta: zeta depth: depth", } ) +# fmt: on pomds["depth"] = 175.0 pomds["zeta"] = ("ocean_time", [-0.155356, -0.127435]) @@ -109,15 +109,14 @@ romsds = xr.Dataset() +# fmt: off romsds["s_rho"] = ( - # fmt: off "s_rho", [-0.983333, -0.95 , -0.916667, -0.883333, -0.85 , -0.816667, -0.783333, -0.75 , -0.716667, -0.683333, -0.65 , -0.616667, -0.583333, -0.55 , -0.516667, -0.483333, -0.45 , -0.416667, -0.383333, -0.35 , -0.316667, -0.283333, -0.25 , -0.216667, -0.183333, -0.15 , -0.116667, -0.083333, -0.05 , -0.016667], - # fmt: on { "long_name": "S-coordinate at RHO-points", "valid_min": -1.0, @@ -125,13 +124,14 @@ "standard_name": "ocean_s_coordinate_g2", "formula_terms": "s: s_rho C: Cs_r eta: zeta depth: h depth_c: hc", "field": "s_rho, scalar", - } + }, ) +# fmt: on romsds.coords["hc"] = 20.0 romsds.coords["h"] = 603.9 romsds.coords["Vtransform"] = 2.0 +# fmt: off romsds.coords["Cs_r"] = ( - # fmt: off "s_rho", [-9.33010396e-01, -8.09234736e-01, -6.98779853e-01, -6.01008926e-01, -5.15058562e-01, -4.39938913e-01, -3.74609181e-01, -3.18031817e-01, @@ -141,8 +141,8 @@ -2.53860004e-02, -1.95414261e-02, -1.46880431e-02, -1.06952600e-02, -7.45515186e-03, -4.87981407e-03, -2.89916971e-03, -1.45919898e-03, -5.20560097e-04, -5.75774004e-05], - # fmt: on ) +# fmt: on romsds["zeta"] = ("ocean_time", [-0.155356, -0.127435]) romsds["temp"] = ( ("ocean_time", "s_rho"), diff --git a/cf_xarray/geometry.py b/cf_xarray/geometry.py index 6286b301..e6854448 100644 --- a/cf_xarray/geometry.py +++ b/cf_xarray/geometry.py @@ -4,6 +4,7 @@ from collections import ChainMap from collections.abc import Hashable, Sequence from dataclasses import dataclass +from typing import TYPE_CHECKING import numpy as np import pandas as pd @@ -21,6 +22,9 @@ ] +if TYPE_CHECKING: + from shapely import MultiPoint, Point + # Useful convention language: # 1. Whether linked to normal CF space-time coordinates with a nodes attribute or not, inclusion of such coordinates is # recommended to maintain backward compatibility with software that has not implemented geometry capabilities. @@ -547,7 +551,11 @@ def cf_to_shapely(ds: xr.Dataset, *, container: Hashable = GEOMETRY_CONTAINER_NA return geometries.rename("geometry") -def points_to_cf(pts: xr.DataArray | Sequence, *, names: GeometryNames | None = None): +def points_to_cf( + pts: xr.DataArray | Sequence[Point | MultiPoint], + *, + names: GeometryNames | None = None, +): """Get a list of points (shapely.geometry.[Multi]Point) and return a CF-compliant geometry dataset. Parameters @@ -563,6 +571,7 @@ def points_to_cf(pts: xr.DataArray | Sequence, *, names: GeometryNames | None = """ from shapely.geometry import MultiPoint + pts_: Sequence[Point | MultiPoint] if isinstance(pts, xr.DataArray): # TODO: Fix this hardcoding if pts.ndim != 1: diff --git a/cf_xarray/options.py b/cf_xarray/options.py index 1b9b5b5e..a69ba6e5 100644 --- a/cf_xarray/options.py +++ b/cf_xarray/options.py @@ -38,7 +38,6 @@ class set_options: # numpydoc ignore=PR01,PR02 >>> ds = xr.Dataset({"elev": np.arange(1000)}) >>> with cf_xarray.set_options(custom_criteria=my_custom_criteria): ... xr.testing.assert_identical(ds["elev"], ds.cf["ssh"]) - ... Or to set global options: diff --git a/cf_xarray/tests/__init__.py b/cf_xarray/tests/__init__.py index f74aad33..8c83df3a 100644 --- a/cf_xarray/tests/__init__.py +++ b/cf_xarray/tests/__init__.py @@ -32,8 +32,7 @@ def __call__(self, dsk, keys, **kwargs): self.total_computes += 1 if self.total_computes > self.max_computes: raise RuntimeError( - "Too many computes. Total: %d > max: %d." - % (self.total_computes, self.max_computes) + f"Too many computes. Total:{self.total_computes} > max: {self.max_computes}." ) return dask.get(dsk, keys, **kwargs) diff --git a/cf_xarray/tests/test_accessor.py b/cf_xarray/tests/test_accessor.py index 36f95016..465c6f3f 100644 --- a/cf_xarray/tests/test_accessor.py +++ b/cf_xarray/tests/test_accessor.py @@ -453,7 +453,7 @@ def test_rename_like() -> None: @pytest.mark.parametrize( "attr, xrkwargs, cfkwargs", ( - ("resample", {"time": "M"}, {"T": "M"}), + ("resample", {"time": "ME"}, {"T": "ME"}), ("rolling", {"lat": 5}, {"Y": 5}), ("groupby", {"group": "time"}, {"group": "T"}), ("groupby", {"group": "time.month"}, {"group": "T.month"}), @@ -1748,7 +1748,7 @@ def test_add_canonical_attributes(override, skip, verbose, capsys): # Attributes have been added for var in sum(ds.cf.standard_names.values(), []): - assert set(ds[var].attrs) < set(cf_ds[var].attrs) + assert set(ds[var].attrs) <= set(cf_ds[var].attrs) # Time units did not change assert ds["time"].attrs.get("units") is cf_ds["time"].attrs.get("units") is None @@ -2047,7 +2047,8 @@ def test_ancillary_variables_extra_dim(): ), } ) - assert_identical(ds.cf["X"], ds["x"]) + with pytest.warns(UserWarning): + assert_identical(ds.cf["X"], ds["x"]) def test_geometry_association(geometry_ds): diff --git a/doc/bounds.md b/doc/bounds.md index 21d85fcd..fe39abf6 100644 --- a/doc/bounds.md +++ b/doc/bounds.md @@ -14,7 +14,7 @@ `cf_xarray` supports parsing [coordinate bounds](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#cell-boundaries) as encoded in the CF `bounds` attribute. A useful feature for incomplete dataset is also the automatic bounds estimation possible through `cf.add_bounds`. This method will estimate the missing bounds by finding the middle points between elements of the given coordinate, but also by extrapolating to find the outer bounds of the grid. This linear estimation works well with rectilinear grids, but it is only a coarse approximation for curvilinear and simple irregular grids. -As an example, we present a "rotated pole" grid. It is defined on a rotated rectilinear grid which uses the `rlat` and `rlon` 1D coordinates, over North America at a resolution of 0.44°. The datasets comes with 2D `lat` and `lon` coordinates. `cf_xarray` will estimate the bounds by linear interpolation (extrapolation at the edges) of the existing `lon` and `lat`, which yields good results on parts of the grid where the rotation is small. However the errors is larger in other places, as seen when visualizing the distance in degrees between the estimated bounds and the true bounds. +As an example, we present a "rotated pole" grid. It is defined on a rotated rectilinear grid which uses the `rlat` and `rlon` 1D coordinates, over North America at a resolution of 0.44°. The datasets comes with 2D `lat` and `lon` coordinates. `cf_xarray` will estimate the bounds by linear interpolation (extrapolation at the edges) of the existing `lon` and `lat`, which yields good results on parts of the grid where the rotation is small. However the errors is larger in other places, as seen when visualizing the distance in degrees between the estimated bounds and the true bounds. ![2d bounds error](2D_bounds_error.png) diff --git a/doc/coord_axes.md b/doc/coord_axes.md index 68665d82..f5fb07bc 100644 --- a/doc/coord_axes.md +++ b/doc/coord_axes.md @@ -55,7 +55,7 @@ This table lists these internal criteria :stub-columns: 1 ``` -Any DataArray that has `standard_name: "latitude"` or `_CoordinateAxisType: "Lat"` or `"units": "degrees_north"` in its `attrs` will be identified as the `"latitude"` variable by cf-xarray. Similarly for other coordinate names. +Any DataArray that has `standard_name: "latitude"` or `_CoordinateAxisType: "Lat"` or `"units": "degrees_north"` in its `attrs` will be identified as the `"latitude"` variable by cf-xarray. Similarly for other coordinate names. ## Axis Names @@ -68,7 +68,7 @@ Similar criteria exist for the concept of "axes". :stub-columns: 1 ``` -## `.axes` and `.coordinates` properties +## `.axes` and `.coordinates` properties Alternatively use the special properties {py:attr}`DataArray.cf.axes` or {py:attr}`DataArray.cf.coordinates` to access the variable names. These properties return dictionaries that map "CF names" to a list of variable names. Note that a list is always returned even if only one variable name matches the name `"latitude"` (for example). diff --git a/doc/custom-criteria.md b/doc/custom-criteria.md index f6daa08d..4faa8437 100644 --- a/doc/custom-criteria.md +++ b/doc/custom-criteria.md @@ -73,7 +73,7 @@ with cfxr.set_options(custom_criteria=salt_criteria): salty ``` -Note that `salty` contains both `salt1` and `salt2`. Without setting these criteria, we would only get `salt1` by default +Note that `salty` contains both `salt1` and `salt2`. Without setting these criteria, we would only get `salt1` by default ```{code-cell} ds.cf[["sea_water_salinity"]] diff --git a/doc/selecting.md b/doc/selecting.md index 0a06e25f..8be19c38 100644 --- a/doc/selecting.md +++ b/doc/selecting.md @@ -30,7 +30,7 @@ CF conventions on 1. [ancillary data](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#ancillary-data) ``` -A powerful feature of `cf_xarray` is the ability select DataArrays using special "CF names" like the "latitude", or "longitude" coordinate names, "X" or "Y" axes names, oreven using the `standard_name` attribute if present. +A powerful feature of `cf_xarray` is the ability select DataArrays using special "CF names" like the "latitude", or "longitude" coordinate names, "X" or "Y" axes names, oreven using the `standard_name` attribute if present. To demonstrate this, let's load a few datasets @@ -91,7 +91,7 @@ anc ## Selecting multiple variables -Sometimes a Dataset may contain multiple `X` or multiple `longitude` variables. In that case a simple `.cf["X"]` will raise an error. Instead follow Xarray convention and pass a list `.cf[["X"]]` to receive a Dataset with all available `"X"` variables +Sometimes a Dataset may contain multiple `X` or multiple `longitude` variables. In that case a simple `.cf["X"]` will raise an error. Instead follow Xarray convention and pass a list `.cf[["X"]]` to receive a Dataset with all available `"X"` variables ```{code-cell} multiple.cf[["X"]] @@ -103,7 +103,7 @@ pop.cf[["longitude"]] ## Mixing names -cf_xarray aims to be as friendly as possible, so it is possible to mix "CF names" and normal variable names. Here we select `UVEL` and `TEMP` by using the `standard_name` of `TEMP` (which is `sea_water_potential_temperature`) +cf_xarray aims to be as friendly as possible, so it is possible to mix "CF names" and normal variable names. Here we select `UVEL` and `TEMP` by using the `standard_name` of `TEMP` (which is `sea_water_potential_temperature`) ```{code-cell} pop.cf[["sea_water_potential_temperature", "UVEL"]] diff --git a/doc/sgrid_ugrid.md b/doc/sgrid_ugrid.md index 6e840048..313aee1b 100644 --- a/doc/sgrid_ugrid.md +++ b/doc/sgrid_ugrid.md @@ -65,7 +65,7 @@ variable `grid` list many more dimension names. ### Topology variable -`cf_xarray` supports identifying the `mesh_topology` variable using the `cf_role` attribute. +`cf_xarray` supports identifying the `mesh_topology` variable using the `cf_role` attribute. ## More? diff --git a/doc/units.md b/doc/units.md index 884828fd..67248cc5 100644 --- a/doc/units.md +++ b/doc/units.md @@ -14,9 +14,9 @@ hide-toc: true 1. [CF conventions on units](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#units) ``` -The xarray ecosystem supports unit-aware arrays using [pint](https://pint.readthedocs.io) and [pint-xarray](https://pint-xarray.readthedocs.io). Some changes are required to make these packages work well with [UDUNITS format recommended by the CF conventions](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#units). +The xarray ecosystem supports unit-aware arrays using [pint](https://pint.readthedocs.io) and [pint-xarray](https://pint-xarray.readthedocs.io). Some changes are required to make these packages work well with [UDUNITS format recommended by the CF conventions](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#units). -`cf_xarray` makes those recommended changes when you `import cf_xarray.units`. These changes allow pint to parse and format UDUNIT units strings, and add several custom units like `degrees_north` for latitude, `psu` for ocean salinity, etc. Be aware that pint supports some units that UDUNITS does not recognize but `cf-xarray` will not try to detect them and raise an error. For example, a temperature subtraction returns "delta_degC" units in pint, which does not exist in UDUNITS. +`cf_xarray` makes those recommended changes when you `import cf_xarray.units`. These changes allow pint to parse and format UDUNIT units strings, and add several custom units like `degrees_north` for latitude, `psu` for ocean salinity, etc. Be aware that pint supports some units that UDUNITS does not recognize but `cf-xarray` will not try to detect them and raise an error. For example, a temperature subtraction returns "delta_degC" units in pint, which does not exist in UDUNITS. ## Formatting units diff --git a/pyproject.toml b/pyproject.toml index e706ecec..f167b774 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,9 +53,6 @@ write_to = "cf_xarray/_version.py" write_to_template= '__version__ = "{version}"' tag_regex= "^(?Pv)?(?P[^\\+]+)(?P.*)?$" -[tool.black] -target-version = ["py310"] - [tool.ruff] target-version = "py310" builtins = ["ellipsis"] @@ -66,14 +63,18 @@ exclude = [ [tool.ruff.lint] # E402: module level import not at top of file -# E501: line too long - let ruff worry about that +# E501: line too long - let black worry about that +# E731: do not assign a lambda expression, use a def ignore = [ "E402", "E501", + "E731", "B018", "B015", ] select = [ + # Bugbear + "B", # Pyflakes "F", # Pycodestyle @@ -84,9 +85,6 @@ select = [ # Pyupgrade "UP", ] -extend-select = [ - "B", # flake8-bugbear -] [tool.ruff.lint.isort] known-first-party = ["cf_xarray"] @@ -103,6 +101,10 @@ known-third-party = [ "xarray" ] +[tool.ruff.format] +# Enable reformatting of code snippets in docstrings. +docstring-code-format = true + [tool.pytest] python_files = "test_*.py"