From 3f0e703afbbc6d5a6d171f216ee93656ba6d34e1 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Sat, 14 Mar 2026 15:57:00 +0000 Subject: [PATCH 1/7] dev --- cf/mixin/fielddomain.py | 27 ++++++++++++++++ cf/mixin/fielddomainlist.py | 26 +++++++++++++++ cf/test/test_xarray.py | 64 +++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 cf/test/test_xarray.py diff --git a/cf/mixin/fielddomain.py b/cf/mixin/fielddomain.py index 60098d76cd..b94739a7d6 100644 --- a/cf/mixin/fielddomain.py +++ b/cf/mixin/fielddomain.py @@ -3014,6 +3014,33 @@ def set_coordinate_reference( return self.set_construct(ref, key=key, copy=False) + def to_xarray(self): + """Convert the {{class}} to an `xarray` Dataset. + + If the `cf_xarray` package (https://cf-xarray.readthedocs.io) + is installed then the `cf_xarray` accessors will be present on + the returned `xarray` objects (`xarray.DataArray.cf` and + `xarray.Dataset.cf`) that allow some interpretation of CF + attributes. + + Note that multiple fields and domains may be written to the + same `xarray` dataset with `cf.write`, e.g. `ds = cf.write([x, + y], fmt='XARRAY')` + + .. versionadded:: NEXTVERSION + + .. seealso:: `cf.write` + + :Returns: + + `xarray.Dataset` + The equivalent `xarray` Dataset. + + """ + from cf.read_write import write + + return write(self, fmt="XARRAY") + # ---------------------------------------------------------------- # Aliases # ---------------------------------------------------------------- diff --git a/cf/mixin/fielddomainlist.py b/cf/mixin/fielddomainlist.py index 1e754ed357..fa376367c4 100644 --- a/cf/mixin/fielddomainlist.py +++ b/cf/mixin/fielddomainlist.py @@ -331,3 +331,29 @@ def select_by_rank(self, *ranks): """ return type(self)(f for f in self if f.match_by_rank(*ranks)) + + def to_xarray(self): + """Convert the list elements to an `xarray` Dataset. + + If the `cf_xarray` package (https://cf-xarray.readthedocs.io) + is installed then the `cf_xarray` accessors will be present on + the returned `xarray` objects (`xarray.DataArray.cf` and + `xarray.Dataset.cf`) that allow some interpretation of CF + attributes. + + Note that ``ds = fl.to_xarray()`` is identical to ``ds = + cf.write(fl, fmt='XARRAY')`` + + .. versionadded:: NEXTVERSION + + .. seealso:: `cf.write` + + :Returns: + + `xarray.Dataset` + The equivalent `xarray` Dataset. + + """ + from cf.read_write import write + + return write(self, fmt="XARRAY") diff --git a/cf/test/test_xarray.py b/cf/test/test_xarray.py new file mode 100644 index 0000000000..66be79c639 --- /dev/null +++ b/cf/test/test_xarray.py @@ -0,0 +1,64 @@ +import datetime +import faulthandler +import unittest + +faulthandler.enable() # to debug seg faults and timeouts + +import xarray as xr + +import cf + + +class xarrayTest(unittest.TestCase): + """Unit test for converting to xarray.""" + + def setUp(self): + """Preparations called immediately before each test method.""" + # Disable log messages to silence expected warnings + cf.log_level("DISABLE") + # Note: to enable all messages for given methods, lines or + # calls (those without a 'verbose' option to do the same) + # e.g. to debug them, wrap them (for methods, start-to-end + # internally) as follows: + # + # cf.LOG_LEVEL('DEBUG') + # < ... test code ... > + # cf.log_level('DISABLE') + + def test_Field_to_xarray(self): + """Test Field.to_xarray.""" + fields = cf.example_fields() + + # Write each field to a different xarray dataset + for f in fields: + ds = f.to_xarray() + self.assertIsInstance(ds, xr.Dataset) + str(ds) + self.assertIn("Conventions", ds.attrs) + + # Write all fields to one xarray dataset + ds = cf.write(fields, fmt="XARRAY") + self.assertIsInstance(ds, xr.Dataset) + str(ds) + + def test_Domain_to_xarray(self): + """Test Domain.to_xarray.""" + domains = [f.domain for f in cf.example_fields()] + + # Write each domain to a different xarray dataset + for d in domains: + ds = d.to_xarray() + self.assertIsInstance(ds, xr.Dataset) + str(ds) + + # Write all domains to one xarray dataset + ds = cf.write(domains, fmt="XARRAY") + self.assertIsInstance(ds, xr.Dataset) + str(ds) + + +if __name__ == "__main__": + print("Run date:", datetime.datetime.now()) + cf.environment() + print("") + unittest.main(verbosity=2) From 00364872643716e0b2f028bcee7b7be565fb6a35 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Sun, 15 Mar 2026 12:14:46 +0000 Subject: [PATCH 2/7] dev --- cf/mixin/fielddomain.py | 15 +++--- cf/mixin/fielddomainlist.py | 4 +- cf/mixin/propertiesdatabounds.py | 4 +- cf/test/test_xarray.py | 50 +++++++++++++++++++ .../source/recipes-source/plot_08_recipe.py | 3 +- .../source/recipes-source/plot_12_recipe.py | 2 +- .../source/recipes-source/plot_13_recipe.py | 4 +- .../source/recipes-source/plot_17_recipe.py | 2 +- .../source/recipes-source/plot_18_recipe.py | 4 +- .../source/recipes-source/plot_19_recipe.py | 3 +- .../source/recipes-source/plot_23_recipe.py | 6 +-- setup.py | 47 +++++++---------- 12 files changed, 93 insertions(+), 51 deletions(-) diff --git a/cf/mixin/fielddomain.py b/cf/mixin/fielddomain.py index b94739a7d6..44ff078565 100644 --- a/cf/mixin/fielddomain.py +++ b/cf/mixin/fielddomain.py @@ -3020,16 +3020,19 @@ def to_xarray(self): If the `cf_xarray` package (https://cf-xarray.readthedocs.io) is installed then the `cf_xarray` accessors will be present on the returned `xarray` objects (`xarray.DataArray.cf` and - `xarray.Dataset.cf`) that allow some interpretation of CF - attributes. + `xarray.Dataset.cf`) that allow some extra interpretation of + CF attributes. - Note that multiple fields and domains may be written to the - same `xarray` dataset with `cf.write`, e.g. `ds = cf.write([x, - y], fmt='XARRAY')` + Note that `f.to_xarray()` is identical to ``ds = cf.write(f, + 'fmt='XARRAY')``; and multiple fields and domains may be + written to the same `xarray` dataset from a `cf.{{class}}List` + (e.g. ``ds = fl.to_xarray()``) or with `cf.write` (e.g.``ds = + cf.write([f, g], fmt='XARRAY')`` or ``ds = cf.write(fl, + fmt='XARRAY')``). .. versionadded:: NEXTVERSION - .. seealso:: `cf.write` + .. seealso:: `cf.{{class}}List.to_xarray`, `cf.write` :Returns: diff --git a/cf/mixin/fielddomainlist.py b/cf/mixin/fielddomainlist.py index fa376367c4..7f4b33fc5e 100644 --- a/cf/mixin/fielddomainlist.py +++ b/cf/mixin/fielddomainlist.py @@ -338,8 +338,8 @@ def to_xarray(self): If the `cf_xarray` package (https://cf-xarray.readthedocs.io) is installed then the `cf_xarray` accessors will be present on the returned `xarray` objects (`xarray.DataArray.cf` and - `xarray.Dataset.cf`) that allow some interpretation of CF - attributes. + `xarray.Dataset.cf`) that allow some extra interpretation of + CF attributes. Note that ``ds = fl.to_xarray()`` is identical to ``ds = cf.write(fl, fmt='XARRAY')`` diff --git a/cf/mixin/propertiesdatabounds.py b/cf/mixin/propertiesdatabounds.py index 1150449a56..720e2f7c3c 100644 --- a/cf/mixin/propertiesdatabounds.py +++ b/cf/mixin/propertiesdatabounds.py @@ -18,7 +18,9 @@ ) from ..functions import equivalent as cf_equivalent from ..functions import inspect as cf_inspect -from ..functions import parse_indices +from ..functions import ( + parse_indices, +) from ..functions import size as cf_size from ..query import Query from ..units import Units diff --git a/cf/test/test_xarray.py b/cf/test/test_xarray.py index 66be79c639..2243f9c555 100644 --- a/cf/test/test_xarray.py +++ b/cf/test/test_xarray.py @@ -56,6 +56,56 @@ def test_Domain_to_xarray(self): self.assertIsInstance(ds, xr.Dataset) str(ds) + def test_FieldList_to_xarray(self): + """Test Field.to_xarray.""" + fields = cf.example_fields() + ds = fields.to_xarray() + self.assertIsInstance(ds, xr.Dataset) + str(ds) + + def test_DomainList_to_xarray(self): + """Test DomainList.to_xarray.""" + domains = cf.DomainList([f.domain for f in cf.example_fields()]) + ds = domains.to_xarray() + self.assertIsInstance(ds, xr.Dataset) + str(ds) + + def test_FieldList_to_xarray_from_dataset(self): + """Test FieldList.to_xarray from datasets read from disk.""" + for dataset in ( + "example_field_0.nc", + "example_field_0.zarr2", + "example_field_0.zarr3", + "gathered.nc", + "DSG_timeSeries_contiguous.nc", + "DSG_timeSeries_indexed.nc", + "DSG_timeSeriesProfile_indexed_contiguous.nc", + "parent.nc", + "external.nc", + "external_missing.nc", + "combined.nc", + "geometry_1.nc", + "geometry_2.nc", + "geometry_3.nc", + "geometry_4.nc", + "geometry_interior_ring.nc", + "geometry_interior_ring_2.nc", + "string_char.nc", + "subsampled_2.nc", + "ugrid_1.nc", + "ugrid_2.nc", + "ugrid_3.nc", + "test_file.nc", + "extra_data.pp", + "file1.pp", + "umfile.pp", + "wgdos_packed.pp", + ): + f = cf.read(dataset, netcdf_backend="netCDF4") + ds = f.to_xarray() + self.assertIsInstance(ds, xr.Dataset) + str(ds) + if __name__ == "__main__": print("Run date:", datetime.datetime.now()) diff --git a/recipes-docs/source/recipes-source/plot_08_recipe.py b/recipes-docs/source/recipes-source/plot_08_recipe.py index 63427f62a7..6045f51448 100644 --- a/recipes-docs/source/recipes-source/plot_08_recipe.py +++ b/recipes-docs/source/recipes-source/plot_08_recipe.py @@ -9,11 +9,10 @@ # 1. Import cf-python, cf-plot, numpy and scipy.stats: import cfplot as cfp -import cf - import numpy as np import scipy.stats as stats +import cf # %% # 2. Three functions are defined: diff --git a/recipes-docs/source/recipes-source/plot_12_recipe.py b/recipes-docs/source/recipes-source/plot_12_recipe.py index b09db0b29f..5304194b19 100644 --- a/recipes-docs/source/recipes-source/plot_12_recipe.py +++ b/recipes-docs/source/recipes-source/plot_12_recipe.py @@ -13,8 +13,8 @@ # %% # 1. Import cf-python, cf-plot and matplotlib.pyplot: -import matplotlib.pyplot as plt import cfplot as cfp +import matplotlib.pyplot as plt import cf diff --git a/recipes-docs/source/recipes-source/plot_13_recipe.py b/recipes-docs/source/recipes-source/plot_13_recipe.py index bf0398713e..9b658597d8 100644 --- a/recipes-docs/source/recipes-source/plot_13_recipe.py +++ b/recipes-docs/source/recipes-source/plot_13_recipe.py @@ -18,13 +18,11 @@ # in next steps. import cartopy.crs as ccrs -import matplotlib.patches as mpatches - import cfplot as cfp +import matplotlib.patches as mpatches import cf - # %% # 2. Read and select the SST by index and look at its contents: sst = cf.read("~/recipes/ERA5_monthly_averaged_SST.nc")[0] diff --git a/recipes-docs/source/recipes-source/plot_17_recipe.py b/recipes-docs/source/recipes-source/plot_17_recipe.py index c94769e2ba..a66c90b518 100644 --- a/recipes-docs/source/recipes-source/plot_17_recipe.py +++ b/recipes-docs/source/recipes-source/plot_17_recipe.py @@ -11,8 +11,8 @@ # %% # 1. Import cf-python and cf-plot: -import matplotlib.pyplot as plt import cfplot as cfp +import matplotlib.pyplot as plt import cf diff --git a/recipes-docs/source/recipes-source/plot_18_recipe.py b/recipes-docs/source/recipes-source/plot_18_recipe.py index f0eae36e35..3beb9d0db9 100644 --- a/recipes-docs/source/recipes-source/plot_18_recipe.py +++ b/recipes-docs/source/recipes-source/plot_18_recipe.py @@ -10,15 +10,15 @@ """ +import cfplot as cfp + # %% # 1. Import cf-python, cf-plot and other required packages: import matplotlib.pyplot as plt import scipy.stats.mstats as mstats -import cfplot as cfp import cf - # %% # 2. Read the data in and unpack the Fields from FieldLists using indexing. # In our example We are investigating the influence of the land height on diff --git a/recipes-docs/source/recipes-source/plot_19_recipe.py b/recipes-docs/source/recipes-source/plot_19_recipe.py index 02d493dc21..ceb9db1c5c 100644 --- a/recipes-docs/source/recipes-source/plot_19_recipe.py +++ b/recipes-docs/source/recipes-source/plot_19_recipe.py @@ -9,10 +9,11 @@ maxima. """ +import cfplot as cfp + # %% # 1. Import cf-python, cf-plot and other required packages: import matplotlib.pyplot as plt -import cfplot as cfp import cf diff --git a/recipes-docs/source/recipes-source/plot_23_recipe.py b/recipes-docs/source/recipes-source/plot_23_recipe.py index 4ae11c3863..29537803af 100644 --- a/recipes-docs/source/recipes-source/plot_23_recipe.py +++ b/recipes-docs/source/recipes-source/plot_23_recipe.py @@ -18,14 +18,13 @@ # sphinx_gallery_thumbnail_number = 2 # sphinx_gallery_end_ignore +import cfplot as cfp +import dask.array as da import matplotlib.pyplot as plt import numpy as np -import dask.array as da -import cfplot as cfp import cf - # %% # 2. Read example data field constructs, and set region for our plots: @@ -171,4 +170,3 @@ # create your figure with cf-plot with placeholders for your other plots, # then add subplots by accessing the ``cfp.plotvars.master_plot`` object, # and finally redraw the figure containing the new plots. - diff --git a/setup.py b/setup.py index 4620e64a4a..c1ed974391 100755 --- a/setup.py +++ b/setup.py @@ -178,58 +178,46 @@ def compile(): The ``cf`` package can: -* read field and domain constructs from netCDF, CDL, Zarr, PP and UM datasets, - +* read field and domain constructs from netCDF, CDL, Zarr, UM fields file, and PP datasets with a choice of netCDF backends, * be fully flexible with respect to dataset storage chunking, - -* create new field constructs in memory, - -* write and append field and domain constructs to netCDF and Zarr v3 datasets on disk, - +* create new field and domain constructs in memory, +* write and append field and domain constructs to netCDF and Zarr v3 datasets on disk, with control over HDF5 internal file metadata, +* read, write, and manipulate UGRID mesh topologies, +* read, write, and manipulate HEALPix grids, * read, write, and create coordinates defined by geometry cells, - -* read netCDF and CDL datasets containing hierarchical groups, - +* read and write netCDF4 string data-type variables, +* read, write, and create netCDF and CDL datasets containing hierarchical groups, +* read, write, and create data that have been compressed by convention + (i.e. ragged or gathered arrays, or coordinate arrays compressed by + subsampling), whilst presenting a view of the data in its + uncompressed form, +* read and write data that are quantized to eliminate false + precision, +* Convert field and domain constructs to `xarray` datasets in memory, * inspect field constructs, - * test whether two field constructs are the same, - * modify field construct metadata and data, - * create subspaces of field constructs, - * incorporate, and create, metadata stored in external files, - * read, write, and create data that have been compressed by convention (i.e. ragged or gathered arrays, or coordinate arrays compressed by subsampling), whilst presenting a view of the data in its uncompressed form, - * combine field constructs arithmetically, - * manipulate field construct data by arithmetical and trigonometrical operations, - * perform statistical collapses on field constructs, - * perform histogram, percentile and binning operations on field constructs, - * regrid structured grid, mesh and DSG field constructs with (multi-)linear, nearest neighbour, first- and second-order conservative and higher order patch recovery methods, including 3-d regridding, and large-grid support, - * apply convolution filters to field constructs, - * create running means from field constructs, +* apply differential operators to field constructs, and +* create derived quantities (such as relative vorticity). -* apply differential operators to field constructs, - -* create derived quantities (such as relative vorticity), - -* read and write data that are quantized to eliminate false - precision. """ @@ -264,6 +252,9 @@ def compile(): "zarr": [ "zarr>=3.1.3", ], + "xarray": [ + "xarray>=2026.2.0", + ], } setup( From 38bd4cac7da391044a37a2f95974f6aeba4f7c5f Mon Sep 17 00:00:00 2001 From: David Hassell Date: Sun, 15 Mar 2026 12:18:28 +0000 Subject: [PATCH 3/7] dev --- docs/source/class/cf.Domain.rst | 14 ++++++++++++++ docs/source/class/cf.Field.rst | 14 ++++++++++++++ docs/source/class/cf.FieldList.rst | 14 ++++++++++++++ docs/source/installation.rst | 6 ++++++ 4 files changed, 48 insertions(+) diff --git a/docs/source/class/cf.Domain.rst b/docs/source/class/cf.Domain.rst index 7f75c0b3ac..f946e620b8 100644 --- a/docs/source/class/cf.Domain.rst +++ b/docs/source/class/cf.Domain.rst @@ -240,6 +240,20 @@ NetCDF ~cf.Domain.nc_set_global_attribute ~cf.Domain.nc_set_global_attributes +.. _Domain-xarray: + +xarray +------ + +.. rubric:: Methods + +.. autosummary:: + :nosignatures: + :toctree: ../method/ + :template: method.rst + + ~cf.Domain.to_xarray + Groups ^^^^^^ diff --git a/docs/source/class/cf.Field.rst b/docs/source/class/cf.Field.rst index 1c307f362e..fb3c0f3764 100644 --- a/docs/source/class/cf.Field.rst +++ b/docs/source/class/cf.Field.rst @@ -439,6 +439,20 @@ NetCDF ~cf.Field.nc_dataset_chunksizes ~cf.Field.nc_set_dataset_chunksizes +.. _Field-xarray: + +xarray +------ + +.. rubric:: Methods + +.. autosummary:: + :nosignatures: + :toctree: ../method/ + :template: method.rst + + ~cf.Field.to_xarray + Groups ^^^^^^ diff --git a/docs/source/class/cf.FieldList.rst b/docs/source/class/cf.FieldList.rst index dfc462f727..bb75b59535 100644 --- a/docs/source/class/cf.FieldList.rst +++ b/docs/source/class/cf.FieldList.rst @@ -41,6 +41,20 @@ Comparison ~cf.FieldList.equals +.. _FieldList-xarray: + +xarray +------ + +.. rubric:: Methods + +.. autosummary:: + :nosignatures: + :toctree: ../method/ + :template: method.rst + + ~cf.FieldList.to_xarray + Miscellaneous ------------- diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 13c9f0ef88..c444d1bf53 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -271,6 +271,12 @@ environments for which these features are not required. For reading and writing Zarr datasets. +.. rubric:: xarray + +* `xarray `_, version 2026.2.0 or newer. + + For converting fields and domains to `xarray` datasets in memory. + .. rubric:: Regridding * `esmpy `_, previously From afc67c0bc01d0e3d11562c9f3bad66fdbcc0a767 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Mon, 16 Mar 2026 17:59:16 +0000 Subject: [PATCH 4/7] dev --- Changelog.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Changelog.rst b/Changelog.rst index fcc3fbb2de..1bcae724cc 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -3,6 +3,13 @@ Version NEXTVERSION **2026-??-??** +* New methods to convert to `xarray`: `cf.Field.to_xarray`, + `cf.FieldList.to_xarray`, `cf.Domain.to_xarray`, and + `cf.DomainList.to_xarray` + (https://github.com/NCAS-CMS/cf-python/issues/???) +* New output format for `cf.write` that creates an `xarray` dataset in + memory: ``'XARRAY'`` + (https://github.com/NCAS-CMS/cf-python/issues/???) * New keyword parameter to `cf.Data.compute`: ``persist`` (https://github.com/NCAS-CMS/cf-python/issues/929) * New function to control the persistence of computed data: @@ -22,8 +29,11 @@ Version NEXTVERSION (https://github.com/NCAS-CMS/cfdm/issues/391) * Fix for subspacing with cyclic `cf.wi` and `cf.wo` arguments (https://github.com/NCAS-CMS/cf-python/issues/887) +* New optional dependency: ``xarray>=2026.2.0`` * Changed dependency: ``cfdm>=1.13.1.0, <1.13.2.0`` +---- + Version 3.19.0 -------------- From 00437c7f08e8b3f44f7cbdcb92be5dd2b666114a Mon Sep 17 00:00:00 2001 From: David Hassell Date: Tue, 17 Mar 2026 00:47:26 +0000 Subject: [PATCH 5/7] dev --- cf/mixin/fielddomain.py | 43 +++++++++++++++++++------------------ cf/mixin/fielddomainlist.py | 28 +++++++++++++----------- cf/test/test_xarray.py | 33 ++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 34 deletions(-) diff --git a/cf/mixin/fielddomain.py b/cf/mixin/fielddomain.py index 339a91d532..91b7a25342 100644 --- a/cf/mixin/fielddomain.py +++ b/cf/mixin/fielddomain.py @@ -3044,38 +3044,39 @@ def set_coordinate_reference( return self.set_construct(ref, key=key, copy=False) - def to_xarray(self): - """Convert the {{class}} to an `xarray` Dataset. - - If the `cf_xarray` package (https://cf-xarray.readthedocs.io) - is installed then the `cf_xarray` accessors that allow some - interpretation of CF attributes will bxe present on the - returned `xarray` objects (`xarray.DataArray.cf` and - `xarray.Dataset.cf`, but not `xarray.DataTree`). - - Note that ``f.to_xarray()`` is identical to ``ds = cf.write(f, - fmt='XARRAY')``; and multiple fields and domains may be - written to the same `xarray` dataset from a `cf.{{class}}List` - (e.g. ``ds = fl.to_xarray()``) or with `cf.write` (e.g.``ds = - cf.write([f, g], fmt='XARRAY')`` or ``ds = cf.write(fl, - fmt='XARRAY')``). + def to_xarray(self, group=True): + """Convert the {{class}} to an `xarray` dataset. + + {{cf_xarray description}} + + Note that ``ds = f.to_xarray()`` is identical to ``ds = + cf.write(f, fmt='XARRAY')``; and multiple fields and domains + may be written to the same `xarray` dataset from a + `cf.{{class}}List` (e.g. ``ds = fl.to_xarray()``) or with + `cf.write`, e.g. ``ds = cf.write([f, g], fmt='XARRAY')``. .. versionadded:: NEXTVERSION .. seealso:: `cf.{{class}}List.to_xarray`, `cf.write` + :Parameter: + + group: `bool`, optional + If False then create a "flat" dataset, i.e. one with + only the root group, regardless of any group structure + specified by the field constructs. If True (the + default) then any sub-groups defined by the netCDF + interface of the {{class}} constructs and its + components will be created and populated. + :Returns: - `xarray.Dataset` or `xarray.DataTree` - The equivalent `xarray` dataset. If there are no - sub-groups of the root group then an `xarray.Dataset` - is returned, oterwise an `xarray.DataTree` is - returned. + {{Returns xarray}} """ from cf.read_write import write - return write(self, fmt="XARRAY") + return write(self, fmt="XARRAY", group=group) # ---------------------------------------------------------------- # Aliases diff --git a/cf/mixin/fielddomainlist.py b/cf/mixin/fielddomainlist.py index da8dd6b52b..a8d8fe130d 100644 --- a/cf/mixin/fielddomainlist.py +++ b/cf/mixin/fielddomainlist.py @@ -332,31 +332,33 @@ def select_by_rank(self, *ranks): return type(self)(f for f in self if f.match_by_rank(*ranks)) - def to_xarray(self): + def to_xarray(self, group=True): """Convert the list elements to an `xarray` Dataset. - If the `cf_xarray` package (https://cf-xarray.readthedocs.io) - is installed then the `cf_xarray` accessors that allow some - interpretation of CF attributes will bxe present on the - returned `xarray` objects (`xarray.DataArray.cf` and - `xarray.Dataset.cf`, but not `xarray.DataTree`). + {{cf_xarray description}} Note that ``ds = fl.to_xarray()`` is identical to ``ds = - cf.write(fl, fmt='XARRAY')`` + cf.write(fl, fmt='XARRAY')``. .. versionadded:: NEXTVERSION .. seealso:: `cf.write` + :Parameter: + + group: `bool`, optional + If False then create a "flat" dataset, i.e. one with + only the root group, regardless of any group structure + specified by the field constructs. If True (the + default) then any sub-groups defined by the netCDF + interface of the constructs and their components will + be created and populated. + :Returns: - `xarray.Dataset` or `xarray.DataTree` - The equivalent `xarray` dataset. If there are no - sub-groups of the root group then an `xarray.Dataset` - is returned, oterwise an `xarray.DataTree` is - returned. + {{Returns xarray}} """ from cf.read_write import write - return write(self, fmt="XARRAY") + return write(self, fmt="XARRAY", group=group) diff --git a/cf/test/test_xarray.py b/cf/test/test_xarray.py index 2e80364c47..619f453c5e 100644 --- a/cf/test/test_xarray.py +++ b/cf/test/test_xarray.py @@ -117,8 +117,10 @@ def test_Field_to_xarray_groups(self): f.nc_set_variable("/forecast/model/q2") ds = f.to_xarray() self.assertIsInstance(ds, xr.DataTree) + self.assertIn("q2", ds["/forecast/model"]) str(ds) + # group=True ds = cf.write([f, g], fmt="XARRAY") self.assertIsInstance(ds, xr.DataTree) str(ds) @@ -126,6 +128,37 @@ def test_Field_to_xarray_groups(self): self.assertIn("q", ds) self.assertIn("q2", ds["/forecast/model"]) + # group=False + ds = f.to_xarray(group=False) + self.assertIsInstance(ds, xr.Dataset) + self.assertIn("q2", ds) + str(ds) + + ds = cf.write([f, g], fmt="XARRAY", group=False) + self.assertIsInstance(ds, xr.Dataset) + str(ds) + + self.assertIn("q", ds) + self.assertIn("q2", ds) + + def test_FieldList_to_xarray_groups(self): + """Test Field.to_xarray with groups.""" + f = cf.example_fields(0) + + ds = f.to_xarray() + self.assertIsInstance(ds, xr.Dataset) + + f[0].nc_set_variable("/forecast/model/q2") + ds = f.to_xarray() + self.assertIsInstance(ds, xr.DataTree) + self.assertIn("q2", ds["/forecast/model"]) + str(ds) + + ds = f.to_xarray(group=False) + self.assertIsInstance(ds, xr.Dataset) + self.assertIn("q2", ds) + str(ds) + def test_Field_to_xarray_aggregation(self): """Test Field.to_xarray with aggregated data.""" f = cf.read("example_field_0.nc")[0] From 222a614814cb1a31e1f717717d0dc28c3d8a5a0e Mon Sep 17 00:00:00 2001 From: David Hassell Date: Tue, 17 Mar 2026 13:32:22 +0000 Subject: [PATCH 6/7] dev --- cf/mixin/fielddomain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cf/mixin/fielddomain.py b/cf/mixin/fielddomain.py index 91b7a25342..6f73a5e224 100644 --- a/cf/mixin/fielddomain.py +++ b/cf/mixin/fielddomain.py @@ -3050,7 +3050,7 @@ def to_xarray(self, group=True): {{cf_xarray description}} Note that ``ds = f.to_xarray()`` is identical to ``ds = - cf.write(f, fmt='XARRAY')``; and multiple fields and domains + cf.write(f, fmt='XARRAY')``; and multiple {{clasfields and domains may be written to the same `xarray` dataset from a `cf.{{class}}List` (e.g. ``ds = fl.to_xarray()``) or with `cf.write`, e.g. ``ds = cf.write([f, g], fmt='XARRAY')``. From 9b1d5603b8a7314c533dbcdc2d5fdc5977c2cd55 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Tue, 17 Mar 2026 14:49:11 +0000 Subject: [PATCH 7/7] dev --- Changelog.rst | 4 ++-- cf/mixin/fielddomain.py | 19 +++++++++++-------- cf/mixin/fielddomainlist.py | 11 ++++++----- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Changelog.rst b/Changelog.rst index 1bcae724cc..3db9936f2e 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -6,10 +6,10 @@ Version NEXTVERSION * New methods to convert to `xarray`: `cf.Field.to_xarray`, `cf.FieldList.to_xarray`, `cf.Domain.to_xarray`, and `cf.DomainList.to_xarray` - (https://github.com/NCAS-CMS/cf-python/issues/???) + (https://github.com/NCAS-CMS/cf-python/issues/933) * New output format for `cf.write` that creates an `xarray` dataset in memory: ``'XARRAY'`` - (https://github.com/NCAS-CMS/cf-python/issues/???) + (https://github.com/NCAS-CMS/cf-python/issues/933) * New keyword parameter to `cf.Data.compute`: ``persist`` (https://github.com/NCAS-CMS/cf-python/issues/929) * New function to control the persistence of computed data: diff --git a/cf/mixin/fielddomain.py b/cf/mixin/fielddomain.py index 6f73a5e224..f5137b5d4a 100644 --- a/cf/mixin/fielddomain.py +++ b/cf/mixin/fielddomain.py @@ -3050,10 +3050,12 @@ def to_xarray(self, group=True): {{cf_xarray description}} Note that ``ds = f.to_xarray()`` is identical to ``ds = - cf.write(f, fmt='XARRAY')``; and multiple {{clasfields and domains - may be written to the same `xarray` dataset from a - `cf.{{class}}List` (e.g. ``ds = fl.to_xarray()``) or with - `cf.write`, e.g. ``ds = cf.write([f, g], fmt='XARRAY')``. + cf.write(f, fmt='XARRAY')``; and multiple {{class_lower}}s may + be written to the same `xarray` dataset with + `cf.{{class}}List.to_xarray`, or with `cf.write` (e.g. ``ds = + cf.write([f, g], fmt='XARRAY')``). Also, `cf.write` allows a + mixture a mixture of fields and domains to be written to the + same `xarray` dataset. .. versionadded:: NEXTVERSION @@ -3062,12 +3064,13 @@ def to_xarray(self, group=True): :Parameter: group: `bool`, optional + If False then create a "flat" dataset, i.e. one with only the root group, regardless of any group structure - specified by the field constructs. If True (the - default) then any sub-groups defined by the netCDF - interface of the {{class}} constructs and its - components will be created and populated. + specified by the netCDF interfaces of the + {{class_lower}} and its components. If True (the + default) then any sub-groups will be created and + populated. :Returns: diff --git a/cf/mixin/fielddomainlist.py b/cf/mixin/fielddomainlist.py index a8d8fe130d..8d529c274f 100644 --- a/cf/mixin/fielddomainlist.py +++ b/cf/mixin/fielddomainlist.py @@ -338,7 +338,9 @@ def to_xarray(self, group=True): {{cf_xarray description}} Note that ``ds = fl.to_xarray()`` is identical to ``ds = - cf.write(fl, fmt='XARRAY')``. + cf.write(fl, fmt='XARRAY')``. Also, `cfdm.write` allows a + mixture a mixture of fields and domains to be written to the + same `xarray` dataset. .. versionadded:: NEXTVERSION @@ -349,10 +351,9 @@ def to_xarray(self, group=True): group: `bool`, optional If False then create a "flat" dataset, i.e. one with only the root group, regardless of any group structure - specified by the field constructs. If True (the - default) then any sub-groups defined by the netCDF - interface of the constructs and their components will - be created and populated. + specified by the netCDF interfaces of the list + elements and their components. If True (the default) + then any sub-groups will be created and populated. :Returns: