From dd70fcb3c744a4f2a11e50199052b1403ff48da6 Mon Sep 17 00:00:00 2001 From: jswhit2 Date: Fri, 23 Oct 2026 14:22:18 -0600 Subject: [PATCH 1/2] add extra_mode_flag Dataset kwarg for user-defined formats (issue #1490) Add an extra_mode_flag kwarg to Dataset that is ORed into the mode passed to nc_open, so files without a magic number can be opened with a user-defined format (UDF) dispatch layer, such as those provided by the NetCDF Expansion Pack (NEP). Add NEP_UDF_* module constants (names match nep.h). Only allowed for plain read-only opens (mode='r', not diskless, memory or parallel), requires netcdf-c >= 4.10.0. Add tests for the kwarg (opening an mmCIF file with no magic number) and for reading GRIB2 with NEP (test/gfs_sample.grb2, the first 3 messages of pygrib's sampledata/gfs.grb). The NEP read tests are skipped unless the NEP readers are registered with netcdf-c. Co-Authored-By: Claude Opus 5.5 --- MANIFEST.in | 1 + src/netCDF4/__init__.pyi | 11 ++++ src/netCDF4/_netCDF4.pyi | 10 +++ src/netCDF4/_netCDF4.pyx | 47 ++++++++++++++- test/gfs_sample.grb2 | Bin 0 -> 27297 bytes test/test_extra_mode_flag.py | 114 +++++++++++++++++++++++++++++++++++ test/test_nep_grib2.py | 62 +++++++++++++++++++ 7 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 test/gfs_sample.grb2 create mode 100644 test/test_extra_mode_flag.py create mode 100644 test/test_nep_grib2.py diff --git a/MANIFEST.in b/MANIFEST.in index 38a3c86a5..f8188bf58 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -10,6 +10,7 @@ include examples/README.md exclude examples/data include test/*py include test/*nc +include test/*grb2 include src/netCDF4/__init__.py include src/netCDF4/_netCDF4.pyx exclude src/netCDF4/_netCDF4.c diff --git a/src/netCDF4/__init__.pyi b/src/netCDF4/__init__.pyi index bb0c30831..9fd8b3eff 100644 --- a/src/netCDF4/__init__.pyi +++ b/src/netCDF4/__init__.pyi @@ -187,6 +187,16 @@ is_native_big: bool default_encoding: Final = "utf-8" unicode_error: Final = "replace" default_fillvals: dict[str, int | float | str] +NEP_UDF_GEOTIFF_BIGTIFF: Final = 0x0040 +NEP_UDF_GEOTIFF_STANDARD: Final = 0x0080 +NEP_UDF_GRIB2: Final = 0x10000 +NEP_UDF_FITS: Final = 0x80000 +NEP_UDF_CDF: Final = 0x100000 +NEP_UDF_PDS4: Final = 0x200000 +NEP_UDF_DICOM: Final = 0x400000 +NEP_UDF_PDB: Final = 0x800000 +NEP_UDF_MMCIF: Final = 0x1000000 +NEP_UDF_NEXTCDF4: Final = 0x2000000 # date2index, date2num, and num2date are actually provided by cftime and if stubs for # cftime are completed these should be removed. @@ -235,6 +245,7 @@ class Dataset: comm: Any = None, info: Any = None, auto_complex: bool = False, + extra_mode_flag: int | None = None, **kwargs: Any, ): ... @property diff --git a/src/netCDF4/_netCDF4.pyi b/src/netCDF4/_netCDF4.pyi index d0bdb389f..7b865b052 100644 --- a/src/netCDF4/_netCDF4.pyi +++ b/src/netCDF4/_netCDF4.pyi @@ -8,6 +8,16 @@ from . import ( Group, MFDataset, MFTime, + NEP_UDF_CDF, + NEP_UDF_DICOM, + NEP_UDF_FITS, + NEP_UDF_GEOTIFF_BIGTIFF, + NEP_UDF_GEOTIFF_STANDARD, + NEP_UDF_GRIB2, + NEP_UDF_MMCIF, + NEP_UDF_NEXTCDF4, + NEP_UDF_PDB, + NEP_UDF_PDS4, NetCDF4MissingFeatureException, Variable, VLType, diff --git a/src/netCDF4/_netCDF4.pyx b/src/netCDF4/_netCDF4.pyx index c9e46e4ca..a553fbb4b 100644 --- a/src/netCDF4/_netCDF4.pyx +++ b/src/netCDF4/_netCDF4.pyx @@ -1612,6 +1612,23 @@ if _nclibversion >= Version("4.6.2"): else: # prior to 4.6.2 this flag doesn't work, so make the same as NC_DISKLESS NC_PERSIST = NC_DISKLESS +# mode flags for the user-defined formats (UDFs) provided by the +# NetCDF Expansion Pack (NEP), names match those in nep.h. +# Used with the extra_mode_flag Dataset kwarg (requires netcdf-c >= 4.10.0). +NEP_UDF_GEOTIFF_BIGTIFF = 0x0040 # NC_UDF0 +NEP_UDF_GEOTIFF_STANDARD = 0x0080 # NC_UDF1 +NEP_UDF_GRIB2 = 0x10000 # NC_UDF2 +NEP_UDF_FITS = 0x80000 # NC_UDF3 +NEP_UDF_CDF = 0x100000 # NC_UDF4 +NEP_UDF_PDS4 = 0x200000 # NC_UDF5 +NEP_UDF_DICOM = 0x400000 # NC_UDF6 +NEP_UDF_PDB = 0x800000 # NC_UDF7 +NEP_UDF_MMCIF = 0x1000000 # NC_UDF8 +NEP_UDF_NEXTCDF4 = 0x2000000 # NC_UDF9 +_nep_udf_flags = (NEP_UDF_GEOTIFF_BIGTIFF, NEP_UDF_GEOTIFF_STANDARD, + NEP_UDF_GRIB2, NEP_UDF_FITS, NEP_UDF_CDF, NEP_UDF_PDS4, + NEP_UDF_DICOM, NEP_UDF_PDB, NEP_UDF_MMCIF, NEP_UDF_NEXTCDF4) + # next two lines do nothing, preserved for backwards compatibility. default_encoding = 'utf-8' unicode_error = 'replace' @@ -2306,11 +2323,13 @@ strings. def __init__(self, filename, mode='r', clobber=True, format='NETCDF4', diskless=False, persist=False, keepweakref=False, memory=None, encoding=None, parallel=False, - comm=None, info=None, auto_complex=False, **kwargs): + comm=None, info=None, auto_complex=False, + extra_mode_flag=None, **kwargs): """ **`__init__(self, filename, mode="r", clobber=True, diskless=False, persist=False, keepweakref=False, memory=None, encoding=None, - parallel=False, comm=None, info=None, format='NETCDF4')`** + parallel=False, comm=None, info=None, format='NETCDF4', + auto_complex=False, extra_mode_flag=None)`** `Dataset` constructor. @@ -2403,6 +2422,15 @@ strings. means MPI_INFO_NULL will be used. Ignored if `parallel=False`. **`auto_complex`**: if `True`, then automatically convert complex number types + + **`extra_mode_flag`**: integer mode flag selecting a user-defined format + (UDF) dispatch layer, such as those provided by the NetCDF Expansion Pack + (NEP). Only needed for files that cannot be identified by their magic + number. Must be one of the `NEP_UDF_*` module constants (for example + `NEP_UDF_MMCIF`), and the UDF must be registered with netcdf-c (for NEP, + via the .ncrc file). Only allowed with `mode='r'`, and not with + `diskless`, `memory` or `parallel`. Requires netcdf-c >= 4.10.0. + Default `None`. """ cdef int grpid, ierr, numgrps, numdims, numvars, cdef size_t initialsize @@ -2478,6 +2506,19 @@ strings. if write_mode and persist: cmode |= NC_WRITE | NC_PERSIST + # extra mode flag for user-defined formats, only used for plain + # read-only nc_open (cmode == NC_NOWRITE). Need to check read_mode + # too, since NC_CLOBBER (mode='w') is also zero. + if extra_mode_flag is not None: + if _nclibversion < Version("4.10.0"): + raise NetCDF4MissingFeatureException("extra_mode_flag", "4.10.0") + if not read_mode or cmode != NC_NOWRITE or memory is not None or parallel: + raise ValueError("extra_mode_flag only allowed with mode='r', " + "and not with diskless, memory or parallel") + if extra_mode_flag not in _nep_udf_flags: + raise ValueError(f"extra_mode_flag must be one of the NEP_UDF_* " + f"constants, got {extra_mode_flag!r}") + if write_mode: _set_default_format(format=format) if memory is not None: @@ -2521,6 +2562,8 @@ strings. with nogil: ierr = nc_open_par(path, cmode | NC_MPIIO, mpicomm, mpiinfo, &grpid) else: + if extra_mode_flag is not None: + cmode |= extra_mode_flag with nogil: ierr = nc_open(path, cmode, &grpid) diff --git a/test/gfs_sample.grb2 b/test/gfs_sample.grb2 new file mode 100644 index 0000000000000000000000000000000000000000..16a09083c70eb194d8aab4a89dd3d909521c9cbd GIT binary patch literal 27297 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 Version('4.10.0') + +def _write(text): + f = tempfile.NamedTemporaryFile(suffix='.cif', delete=False, mode='w') + f.write(text) + f.close() + return f.name + +def _has_nep_mmcif(): + if not has_udf: + return False + filename = _write(MMCIF) + try: + netCDF4.Dataset(filename).close() + return True + except OSError: + return False + finally: + os.remove(filename) + +has_nep_mmcif = _has_nep_mmcif() + + +@unittest.skipUnless(has_udf, "extra_mode_flag requires netcdf-c >= 4.10.0") +class ExtraModeFlagArgsTestCase(unittest.TestCase): + + def setUp(self): + self.file = tempfile.NamedTemporaryFile(suffix='.nc', delete=False).name + netCDF4.Dataset(self.file, 'w').close() + + def tearDown(self): + os.remove(self.file) + + def runTest(self): + """only allowed for plain read-only opens with a NEP_UDF_* flag""" + flag = netCDF4.NEP_UDF_MMCIF + for kwargs in (dict(mode='w'), dict(mode='a'), dict(mode='r+'), + dict(mode='rs'), dict(mode='r', diskless=True)): + with self.assertRaises(ValueError, msg=str(kwargs)): + netCDF4.Dataset(self.file, extra_mode_flag=flag, **kwargs) + with open(self.file, 'rb') as f: + buf = f.read() + with self.assertRaises(ValueError): + netCDF4.Dataset('inmemory.nc', memory=buf, extra_mode_flag=flag) + # not a NEP_UDF_* flag (NC_WRITE) + with self.assertRaises(ValueError): + netCDF4.Dataset(self.file, extra_mode_flag=0x0001) + # extra_mode_flag=None is the default + netCDF4.Dataset(self.file, extra_mode_flag=None).close() + + +@unittest.skipUnless(has_nep_mmcif, "NEP mmCIF reader not available") +class ExtraModeFlagNEPTestCase(unittest.TestCase): + + def setUp(self): + # without the leading data_ magic number netcdf-c can't detect the format + self.file = _write("# no magic number\n" + MMCIF) + + def tearDown(self): + os.remove(self.file) + + def runTest(self): + """open mmCIF file with no magic number using NEP_UDF_MMCIF""" + with self.assertRaises(OSError): + netCDF4.Dataset(self.file) + with netCDF4.Dataset(self.file, extra_mode_flag=netCDF4.NEP_UDF_MMCIF) as ds: + assert ds.entry_id == 'TINY' + assert len(ds.dimensions['atom']) == 3 + assert_array_almost_equal(ds['atom_site_Cartn_x'][0], CARTN_X) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_nep_grib2.py b/test/test_nep_grib2.py new file mode 100644 index 000000000..3b88cdf87 --- /dev/null +++ b/test/test_nep_grib2.py @@ -0,0 +1,62 @@ +import unittest, os +import numpy as np +from numpy.testing import assert_allclose +import netCDF4 + +# test reading a GRIB2 file with the NetCDF Expansion Pack (NEP) GRIB2 +# reader (issue #1490). Only runs if the NEP GRIB2 reader is registered +# with netcdf-c (e.g. via NCRCENV_RC pointing to NEP's .ncrc). +# gfs_sample.grb2 is the first 3 messages of pygrib's sampledata/gfs.grb +# (10 hPa HGT, TMP and RH on a 2.5 degree global grid). + +FILE_NAME = os.path.join(os.path.dirname(__file__), 'gfs_sample.grb2') +NLATS, NLONS = 73, 144 + +# reference values from pygrib: (value at [0,0], [36,72], [-1,-1], min, max) +REFERENCE = { + 'HGT': (29989.89, 31001.2, 27951.64, 27900.99, 31664.09), + 'TMP': (213.7, 233.1, 221.0, 209.3, 257.1), + 'RH': (0.0, 0.03, 0.06, 0.0, 0.21), +} + +def _has_nep_grib2(): + try: + netCDF4.Dataset(FILE_NAME).close() + return True + except OSError: + return False + +has_nep_grib2 = _has_nep_grib2() + + +@unittest.skipUnless(has_nep_grib2, "NEP GRIB2 reader not available") +class NEPGrib2TestCase(unittest.TestCase): + + def check(self, ds): + assert ds.Conventions == 'GRIB2' + assert ds.GRIB2_edition == 2 + assert list(ds.variables) == list(REFERENCE) + for name, (v00, vmid, vlast, vmin, vmax) in REFERENCE.items(): + var = ds[name] + assert var.long_name == name + data = var[:] + assert data.size == NLATS * NLONS + # NEP currently defines the dimensions in the wrong order + # (y=144, x=73), but the data are in the usual (lat, lon) + # order, so reshape before comparing. + data = data.reshape(NLATS, NLONS) + assert_allclose([data[0, 0], data[NLATS//2, NLONS//2], data[-1, -1], + data.min(), data.max()], + [v00, vmid, vlast, vmin, vmax], rtol=1.e-5, atol=1.e-5) + + def runTest(self): + """read GRIB2 file, with format detected by magic number""" + with netCDF4.Dataset(FILE_NAME) as ds: + self.check(ds) + # selecting the GRIB2 reader explicitly gives the same result + with netCDF4.Dataset(FILE_NAME, extra_mode_flag=netCDF4.NEP_UDF_GRIB2) as ds: + self.check(ds) + + +if __name__ == '__main__': + unittest.main() From 7c18a41a89174a8643ccebe852c86190c00bc9de Mon Sep 17 00:00:00 2001 From: jswhit2 Date: Fri, 23 Oct 2026 15:19:54 -0600 Subject: [PATCH 2/2] boost version number and add Changelog entry. --- Changelog | 5 +++++ src/netCDF4/_netCDF4.pyx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index fb7631133..e00fdd361 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,8 @@ + version 1.7.6 (not yet released) + ================================ + * Allow mode flag (`extra_mode_flag` Dataset kwarg) to be passed to nc_open() to support + additional dispatch layers available via the NetCDF Expansion Pack (NEP) (issue #1490). + version 1.7.5.1.post1 (tag v1.7.5.1.post1rel) ============================================= * fix installation of plugins into linux wheels. diff --git a/src/netCDF4/_netCDF4.pyx b/src/netCDF4/_netCDF4.pyx index a553fbb4b..60f7a755f 100644 --- a/src/netCDF4/_netCDF4.pyx +++ b/src/netCDF4/_netCDF4.pyx @@ -1,4 +1,4 @@ -"""Version 1.7.5.1.post1 +"""Version 1.7.6 ------------- # Introduction @@ -1283,7 +1283,7 @@ import sys import functools from typing import Union -__version__ = "1.7.5.1.post1" +__version__ = "1.7.6" # Initialize numpy import posixpath