h5netcdf 1.6.4__tar.gz → 1.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of h5netcdf might be problematic. Click here for more details.

Files changed (34) hide show
  1. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/.pre-commit-config.yaml +4 -4
  2. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/CHANGELOG.rst +20 -1
  3. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/PKG-INFO +5 -5
  4. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/README.rst +4 -4
  5. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/doc/devguide.rst +2 -1
  6. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/h5netcdf/_version.py +16 -3
  7. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/h5netcdf/attrs.py +24 -2
  8. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/h5netcdf/core.py +176 -41
  9. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/h5netcdf/dimensions.py +21 -5
  10. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/h5netcdf/tests/conftest.py +12 -0
  11. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/h5netcdf/tests/test_h5netcdf.py +321 -146
  12. h5netcdf-1.7.1/h5netcdf/utils.py +231 -0
  13. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/h5netcdf.egg-info/PKG-INFO +5 -5
  14. h5netcdf-1.6.4/h5netcdf/utils.py +0 -26
  15. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/AUTHORS.txt +0 -0
  16. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/LICENSE +0 -0
  17. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/doc/Makefile +0 -0
  18. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/doc/api.rst +0 -0
  19. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/doc/changelog.rst +0 -0
  20. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/doc/conf.py +0 -0
  21. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/doc/feature.rst +0 -0
  22. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/doc/index.rst +0 -0
  23. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/doc/legacyapi.rst +0 -0
  24. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/h5netcdf/__init__.py +0 -0
  25. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/h5netcdf/legacyapi.py +0 -0
  26. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/h5netcdf/tests/pytest.ini +0 -0
  27. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/h5netcdf.egg-info/SOURCES.txt +0 -0
  28. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/h5netcdf.egg-info/dependency_links.txt +0 -0
  29. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/h5netcdf.egg-info/requires.txt +0 -0
  30. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/h5netcdf.egg-info/top_level.txt +0 -0
  31. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/licenses/H5PY_LICENSE.txt +0 -0
  32. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/licenses/PSF_LICENSE.txt +0 -0
  33. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/pyproject.toml +0 -0
  34. {h5netcdf-1.6.4 → h5netcdf-1.7.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  repos:
2
2
  - repo: https://github.com/pre-commit/pre-commit-hooks
3
- rev: v4.6.0
3
+ rev: v6.0.0
4
4
  hooks:
5
5
  - id: trailing-whitespace
6
6
  - id: end-of-file-fixer
@@ -10,16 +10,16 @@ repos:
10
10
  - id: debug-statements
11
11
  - id: mixed-line-ending
12
12
  - repo: https://github.com/astral-sh/ruff-pre-commit
13
- rev: 'v0.5.0'
13
+ rev: 'v0.12.12'
14
14
  hooks:
15
15
  - id: ruff
16
16
  args: [ "--fix" ]
17
17
  - repo: https://github.com/psf/black
18
- rev: 24.4.2
18
+ rev: 25.1.0
19
19
  hooks:
20
20
  - id: black
21
21
  - repo: https://github.com/adamchainz/blacken-docs
22
- rev: "1.18.0"
22
+ rev: "1.20.0"
23
23
  hooks:
24
24
  - id: blacken-docs
25
25
  additional_dependencies:
@@ -1,9 +1,28 @@
1
1
  Change Log
2
2
  ----------
3
3
 
4
+ Version 1.7.1 (October 16th, 2025):
5
+
6
+ - Fix regression where attributes with list of strings were written with h5py low-level API instead of high-level API (:issue:`291`, :pull:`292`).
7
+ By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_
8
+
9
+ Version 1.7.0 (October 15th, 2025):
10
+
11
+ - Fix unintentional changes in test suite (:pull:`277`).
12
+ By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_
13
+ - Create ENUM with low level API to keep order-by-value, add h5dump based tests (:pull:`285`).
14
+ By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_ and `David Huard <https://github.com/huard>`_
15
+ - Fix interoperability issues between netcdf4/h5netcdf, namely resizing variables using partial slices,
16
+ creating string attributes with NULLTERM, proper attachment of _Netcdf4Coordinates and _Netcdf4Dimid as well as special string type fillvalues (:pull:`286`).
17
+ By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_ and `David Huard <https://github.com/huard>`_
18
+ - Add the `format` argument to `h5netcdf.File` and support for the `NETCDF4_CLASSIC` format (:issue:`280`, :pull:`283`).
19
+ By `David Huard <https://github.com/huard>`_
20
+ - Do not return padded arrays for slices larger than variable shape (:issue:`287`, :pull:`288`).
21
+ By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_
22
+
4
23
  Version 1.6.4 (August 5th, 2025):
5
24
 
6
- - Cleanup: pyupgrade --py39-plus
25
+ - Cleanup: pyupgrade --py39-plus (:pull:`272`).
7
26
  By `Kurt Schwehr <https://github.com/schwehr>`_
8
27
  - Add better error messages when operating on a closed file (:issue:`274`, :pull:`275`).
9
28
  By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: h5netcdf
3
- Version: 1.6.4
3
+ Version: 1.7.1
4
4
  Summary: netCDF4 via h5py
5
5
  Author-email: Stephan Hoyer <shoyer@gmail.com>, Kai Mühlbauer <kmuehlbauer@wradlib.org>
6
6
  Maintainer-email: h5netcdf developers <devteam@h5netcdf.org>
@@ -124,10 +124,10 @@ Usage
124
124
  -----
125
125
 
126
126
  h5netcdf has two APIs, a new API and a legacy API. Both interfaces currently
127
- reproduce most of the features of the netCDF interface, with the notable
128
- exception of support for operations that rename or delete existing objects.
129
- We simply haven't gotten around to implementing this yet. Patches
130
- would be very welcome.
127
+ reproduce most of the features of the netCDF interface, including the ability
128
+ to write NETCDF4 and NETCDF4_CLASSIC formatted files. Support for operations
129
+ that rename or delete existing objects is still missing, and patches would be
130
+ very welcome.
131
131
 
132
132
  New API
133
133
  ~~~~~~~
@@ -64,10 +64,10 @@ Usage
64
64
  -----
65
65
 
66
66
  h5netcdf has two APIs, a new API and a legacy API. Both interfaces currently
67
- reproduce most of the features of the netCDF interface, with the notable
68
- exception of support for operations that rename or delete existing objects.
69
- We simply haven't gotten around to implementing this yet. Patches
70
- would be very welcome.
67
+ reproduce most of the features of the netCDF interface, including the ability
68
+ to write NETCDF4 and NETCDF4_CLASSIC formatted files. Support for operations
69
+ that rename or delete existing objects is still missing, and patches would be
70
+ very welcome.
71
71
 
72
72
  New API
73
73
  ~~~~~~~
@@ -13,6 +13,7 @@ Contributors
13
13
  - `Aleksandar Jelenak <https://github.com/ajelenak>`_
14
14
  - `Bas Couwenberg <https://github.com/sebastic>`_.
15
15
  - `Brett Naul <https://github.com/bnaul>`_
16
+ - `David Huard <https://github.com/huard>`_
16
17
  - `Dion Häfner <https://github.com/dionhaefner>`_
17
18
  - `Drew Parsons <https://github.com/drew-parsons>`_
18
19
  - `Ezequiel Cimadevilla Alvarez <https://github.com/zequihg50>`_
@@ -49,7 +50,7 @@ Continuous Integration
49
50
  or a PullRequest branch several checks are performed:
50
51
 
51
52
  - Lint and style checks (``ruff``, ``black``)
52
- - Unit tests with latest ``h5py3`` (Python 3.9, 3.10, 3.11) facilitating GitHub Ubuntu worker
53
+ - Unit tests with latest ``h5py3`` (and Python versions) facilitating GitHub Ubuntu worker
53
54
  - Documentation build, artifacts are made available to download
54
55
  - On release, source-tarball and universal wheel is uploaded to PyPI and documentation is made available
55
56
  on `h5netcdf GitHub Pages`_
@@ -1,7 +1,14 @@
1
1
  # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
3
 
4
- __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
5
12
 
6
13
  TYPE_CHECKING = False
7
14
  if TYPE_CHECKING:
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
9
16
  from typing import Union
10
17
 
11
18
  VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
12
20
  else:
13
21
  VERSION_TUPLE = object
22
+ COMMIT_ID = object
14
23
 
15
24
  version: str
16
25
  __version__: str
17
26
  __version_tuple__: VERSION_TUPLE
18
27
  version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
19
30
 
20
- __version__ = version = '1.6.4'
21
- __version_tuple__ = version_tuple = (1, 6, 4)
31
+ __version__ = version = '1.7.1'
32
+ __version_tuple__ = version_tuple = (1, 7, 1)
33
+
34
+ __commit_id__ = commit_id = 'g251854355'
@@ -2,6 +2,8 @@ from collections.abc import MutableMapping
2
2
 
3
3
  import numpy as np
4
4
 
5
+ from .utils import _create_string_attribute
6
+
5
7
  _HIDDEN_ATTRS = frozenset(
6
8
  [
7
9
  "REFERENCE_LIST",
@@ -17,10 +19,11 @@ _HIDDEN_ATTRS = frozenset(
17
19
 
18
20
 
19
21
  class Attributes(MutableMapping):
20
- def __init__(self, h5attrs, check_dtype, h5py_pckg):
22
+ def __init__(self, h5attrs, check_dtype, h5py_pckg, format="NETCDF4"):
21
23
  self._h5attrs = h5attrs
22
24
  self._check_dtype = check_dtype
23
25
  self._h5py = h5py_pckg
26
+ self._format = format
24
27
 
25
28
  def __getitem__(self, key):
26
29
  if key in _HIDDEN_ATTRS:
@@ -83,7 +86,26 @@ class Attributes(MutableMapping):
83
86
  dtype = np.asarray(value).dtype
84
87
 
85
88
  self._check_dtype(dtype)
86
- self._h5attrs[key] = value
89
+
90
+ is_plain_string = dtype.kind in {"S", "U"} and dtype.metadata is None
91
+ if (
92
+ is_plain_string # for simple strings
93
+ and (not isinstance(value, list) and self._format == "NETCDF4_CLASSIC")
94
+ and not isinstance(value, self._h5py.Empty)
95
+ and self._h5py.__name__ == "h5py"
96
+ ):
97
+ # create with low level API to get fixed length strings
98
+ # as netcdf4-python/netcdf-c does
99
+ _create_string_attribute(self._h5attrs._id, key, value)
100
+ # always for CLASSIC mode
101
+ elif self._format == "NETCDF4_CLASSIC":
102
+ self._h5attrs[key] = np.atleast_1d(value)
103
+ else:
104
+ # netcdf4-python/netcdf-c writes non-string scalars as simple dataset
105
+ # converting to 1D
106
+ if np.isscalar(value) and dtype.kind not in {"S", "U"}:
107
+ value = np.atleast_1d(value)
108
+ self._h5attrs[key] = value
87
109
 
88
110
  def __delitem__(self, key):
89
111
  del self._h5attrs[key]
@@ -13,7 +13,15 @@ from packaging import version
13
13
  from . import __version__
14
14
  from .attrs import Attributes
15
15
  from .dimensions import Dimension, Dimensions
16
- from .utils import Frozen
16
+ from .utils import (
17
+ CompatibilityError,
18
+ Frozen,
19
+ _commit_enum_type,
20
+ _create_classic_string_dataset,
21
+ _create_enum_dataset,
22
+ _create_enum_dataset_attribute,
23
+ _create_string_attribute,
24
+ )
17
25
 
18
26
  try:
19
27
  import h5pyd
@@ -36,10 +44,6 @@ def _name_from_dimension(dim):
36
44
  return dim[0].name.split("/")[-1]
37
45
 
38
46
 
39
- class CompatibilityError(Exception):
40
- """Raised when using features that are not part of the NetCDF4 API."""
41
-
42
-
43
47
  def _invalid_netcdf_feature(feature, allow):
44
48
  if not allow:
45
49
  msg = (
@@ -348,7 +352,9 @@ class BaseVariable(BaseObject):
348
352
  [self._parent._all_dimensions[d]._dimid for d in dims],
349
353
  "int32",
350
354
  )
351
- if len(coord_ids) > 1:
355
+ # add _Netcdf4Coordinates for multi-dimensional coordinate variables
356
+ # or for (one-dimensional) coordinates
357
+ if len(coord_ids) >= 1:
352
358
  self._h5ds.attrs["_Netcdf4Coordinates"] = coord_ids
353
359
 
354
360
  def _ensure_dim_id(self):
@@ -361,12 +367,21 @@ class BaseVariable(BaseObject):
361
367
  self._h5ds.attrs["_Netcdf4Dimid"] = dim.attrs["_Netcdf4Dimid"]
362
368
 
363
369
  def _maybe_resize_dimensions(self, key, value):
364
- """Resize according to given (expanded) key with respect to variable dimensions"""
370
+ """Resize according to given (expanded) key with respect to variable dimensions.
371
+
372
+ Parameters
373
+ ----------
374
+ key : Tuple[slice]
375
+ Indexing key
376
+ value : array-like
377
+ Values to be written.
378
+ """
365
379
  new_shape = ()
366
- v = None
380
+ v = np.asarray(value)
367
381
  for i, dim in enumerate(self.dimensions):
368
382
  # is unlimited dimensions (check in all dimensions)
369
383
  if self._parent._all_dimensions[dim].isunlimited():
384
+ current_dim_size = len(self._parent._all_dimensions[dim])
370
385
  if key[i].stop is None:
371
386
  # if stop is None, get dimensions from value,
372
387
  # they must match with variable dimension
@@ -375,16 +390,25 @@ class BaseVariable(BaseObject):
375
390
  if v.ndim == self.ndim:
376
391
  new_max = max(v.shape[i], self._h5ds.shape[i])
377
392
  elif v.ndim == 0:
378
- # for scalars we take the current dimension size (check in all dimensions
393
+ # for scalar values we take the current dimension size
394
+ # (check in all dimensions)
379
395
  new_max = self._parent._all_dimensions[dim].size
396
+ # but for compatibility with netcdf4-python/netcdf-c
397
+ # we set at least 1
398
+ if new_max == 0:
399
+ new_max = 1
380
400
  else:
381
401
  raise IndexError("shape of data does not conform to slice")
402
+ # if slice stop is negative, we need to check the value size
403
+ elif key[i].stop < 0:
404
+ new_max = v.shape[i] - key[i].stop
382
405
  else:
383
406
  new_max = max(key[i].stop, self._h5ds.shape[i])
384
407
  # resize unlimited dimension if needed but no other variables
385
408
  # this is in line with `netcdf4-python` which only resizes
386
409
  # the dimension and this variable
387
- if self._parent._all_dimensions[dim].size < new_max:
410
+ # todo: check above assumptions with latest netcdf4-python/netcdf-c
411
+ if current_dim_size < new_max and self.name == dim:
388
412
  self._parent.resize_dimension(dim, new_max)
389
413
  new_shape += (new_max,)
390
414
  else:
@@ -412,8 +436,19 @@ class BaseVariable(BaseObject):
412
436
  string_info
413
437
  and string_info.length is not None
414
438
  and string_info.length > 1
415
- ) or enum_info:
439
+ ):
440
+ # fixed length string
416
441
  value = fillvalue
442
+ elif string_info and string_info.length is None:
443
+ # variable length string
444
+ value = fillvalue
445
+ elif enum_info:
446
+ value = fillvalue
447
+ if self._root._h5py.__name__ == "h5py":
448
+ _create_enum_dataset_attribute(
449
+ self, "_FillValue", value, self.datatype
450
+ )
451
+ return
417
452
  else:
418
453
  value = self.dtype.type(fillvalue)
419
454
 
@@ -499,16 +534,17 @@ class BaseVariable(BaseObject):
499
534
  for k in key0
500
535
  ]
501
536
  # second convert to max shape
537
+ # we take the minimum of shape vs max_index to not return
538
+ # slices larger than expected data
502
539
  max_shape = tuple(
503
- [
504
- shape[i] if k is None else max(h5ds_shape[i], k)
505
- for i, k in enumerate(max_index)
506
- ]
540
+ s if k is None else min(s, k) for s, k in zip(shape, max_index)
507
541
  )
508
542
 
509
543
  # check if hdf5 dataset dimensions are smaller than
510
544
  # their respective netcdf dimensions
511
545
  sdiff = [d0 - d1 for d0, d1 in zip(max_shape, h5ds_shape)]
546
+ # set negative values to zero
547
+ sdiff = np.maximum(sdiff, 0)
512
548
  # create padding only if hdf5 dataset is smaller than netcdf dimension
513
549
  if sum(sdiff):
514
550
  padding = [(0, s) for s in sdiff]
@@ -583,13 +619,28 @@ class BaseVariable(BaseObject):
583
619
  ):
584
620
  self._h5ds[key] = value.view(view)
585
621
  else:
586
- self._h5ds[key] = value
622
+ # write with low-level API for CLASSIC format
623
+ if (
624
+ self._root._format == "NETCDF4_CLASSIC"
625
+ and self.dtype.kind in ["S", "U"]
626
+ and self._root._h5py.__name__ == "h5py"
627
+ ):
628
+ # h5py expects np.ndarray
629
+ value = np.asanyarray(value)
630
+ self._h5ds.id.write(
631
+ h5py.h5s.ALL, h5py.h5s.ALL, value, mtype=self._h5ds.id.get_type()
632
+ )
633
+ else:
634
+ self._h5ds[key] = value
587
635
 
588
636
  @property
589
637
  def attrs(self):
590
638
  """Return variable attributes."""
591
639
  return Attributes(
592
- self._h5ds.attrs, self._root._check_valid_netcdf_dtype, self._root._h5py
640
+ self._h5ds.attrs,
641
+ self._root._check_valid_netcdf_dtype,
642
+ self._root._h5py,
643
+ format=self._root._format,
593
644
  )
594
645
 
595
646
  _cls_name = "h5netcdf.Variable"
@@ -979,6 +1030,13 @@ class Group(Mapping):
979
1030
 
980
1031
  @dimensions.setter
981
1032
  def dimensions(self, value):
1033
+ if self._format == "NETCDF4_CLASSIC":
1034
+ unlimited_dims = list(filter(lambda s: s in [None, 0], value.values()))
1035
+ if len(unlimited_dims) > 1:
1036
+ raise CompatibilityError(
1037
+ "NETCDF4_CLASSIC format only allows one unlimited dimension."
1038
+ )
1039
+
982
1040
  for k, v in self._all_dimensions.maps[0].items():
983
1041
  if k in value:
984
1042
  if v != value[k]:
@@ -1104,8 +1162,10 @@ class Group(Mapping):
1104
1162
  # dimension scale without a corresponding variable.
1105
1163
  # Keep the references, to re-attach later
1106
1164
  refs = None
1165
+ dimid = None
1107
1166
  if h5name in self._dimensions and h5name in self._h5group:
1108
1167
  refs = self._dimensions[name]._scale_refs
1168
+ dimid = self._dimensions[name]._h5ds.attrs.get("_Netcdf4Dimid", None)
1109
1169
  self._dimensions[name]._detach_scale()
1110
1170
  del self._h5group[name]
1111
1171
 
@@ -1115,15 +1175,28 @@ class Group(Mapping):
1115
1175
  fillvalue, h5fillvalue = _check_fillvalue(self, fillvalue, dtype)
1116
1176
 
1117
1177
  # create hdf5 variable
1118
- self._h5group.create_dataset(
1119
- h5name,
1120
- shape,
1121
- dtype=dtype,
1122
- data=data,
1123
- chunks=chunks,
1124
- fillvalue=h5fillvalue,
1125
- **kwargs,
1126
- )
1178
+ # for classic format string types write with low level API
1179
+ if (
1180
+ self._root._format == "NETCDF4_CLASSIC"
1181
+ and np.dtype(dtype).kind in ["S", "U"]
1182
+ and self._root._h5py.__name__ == "h5py"
1183
+ ):
1184
+ _create_classic_string_dataset(
1185
+ self._h5group._id, h5name, data, shape, chunks
1186
+ )
1187
+ elif self._root._h5py.__name__ == "h5py" and isinstance(dtype, EnumType):
1188
+ # use low level API for creating ENUMS
1189
+ _create_enum_dataset(self, h5name, shape, dtype, h5fillvalue)
1190
+ else:
1191
+ self._h5group.create_dataset(
1192
+ h5name,
1193
+ shape,
1194
+ dtype=dtype,
1195
+ data=data,
1196
+ chunks=chunks,
1197
+ fillvalue=h5fillvalue,
1198
+ **kwargs,
1199
+ )
1127
1200
 
1128
1201
  # create variable class instance
1129
1202
  variable = self._variable_cls(self, h5name, dimensions)
@@ -1135,9 +1208,12 @@ class Group(Mapping):
1135
1208
 
1136
1209
  # Re-create dim-scale and re-attach references to coordinate variable.
1137
1210
  if name in self._all_dimensions and h5name in self._h5group:
1138
- self._all_dimensions[name]._create_scale()
1211
+ if dimid is not None:
1212
+ self._all_dimensions[name]._create_scale(dimid=dimid)
1139
1213
  if refs is not None:
1140
1214
  self._all_dimensions[name]._attach_scale(refs)
1215
+ # re-attach coords for dimension scales
1216
+ variable._attach_coords()
1141
1217
 
1142
1218
  # In case of data variables attach dim_scales and coords.
1143
1219
  if name in self.variables and h5name not in self._dimensions:
@@ -1146,9 +1222,12 @@ class Group(Mapping):
1146
1222
 
1147
1223
  # This is a bit of a hack, netCDF4 attaches _Netcdf4Dimid to every variable
1148
1224
  # when a variable is first written to, after variable creation.
1149
- # Here we just attach it to every variable on creation.
1150
- # Todo: get this consistent with netcdf-c/netcdf4-python
1151
- variable._ensure_dim_id()
1225
+ # Last known behaviour since netcdf4-python 1.7.2 and netcdf-c 4.9.2
1226
+ if (None in maxshape and maxshape[0] is not None) or (
1227
+ None not in maxshape
1228
+ and len(variable._h5ds.attrs.get("_Netcdf4Coordinates", [])) >= 1
1229
+ ):
1230
+ variable._ensure_dim_id()
1152
1231
 
1153
1232
  # add fillvalue attribute to variable
1154
1233
  if fillvalue is not None:
@@ -1211,7 +1290,6 @@ class Group(Mapping):
1211
1290
  var : h5netcdf.Variable
1212
1291
  Variable class instance
1213
1292
  """
1214
-
1215
1293
  # if root-variable
1216
1294
  if name.startswith("/"):
1217
1295
  # handling default fillvalues for legacyapi
@@ -1220,6 +1298,12 @@ class Group(Mapping):
1220
1298
 
1221
1299
  if fillvalue is None and isinstance(self._parent._root, Dataset):
1222
1300
  fillvalue = _get_default_fillvalue(dtype)
1301
+
1302
+ if self._root._format == "NETCDF4_CLASSIC" and len(dimensions) == 0:
1303
+ raise CompatibilityError(
1304
+ "NETCDF4_CLASSIC format does not allow variables without dimensions."
1305
+ )
1306
+
1223
1307
  return self._root.create_variable(
1224
1308
  name[1:],
1225
1309
  dimensions,
@@ -1343,7 +1427,10 @@ class Group(Mapping):
1343
1427
  @property
1344
1428
  def attrs(self):
1345
1429
  return Attributes(
1346
- self._h5group.attrs, self._root._check_valid_netcdf_dtype, self._root._h5py
1430
+ self._h5group.attrs,
1431
+ self._root._check_valid_netcdf_dtype,
1432
+ self._root._h5py,
1433
+ format=self._root._format,
1347
1434
  )
1348
1435
 
1349
1436
  _cls_name = "h5netcdf.Group"
@@ -1397,8 +1484,14 @@ class Group(Mapping):
1397
1484
  enum_dict: dict
1398
1485
  A Python dictionary containing the Enum field/value pairs.
1399
1486
  """
1400
- et = self._root._h5py.enum_dtype(enum_dict, basetype=datatype)
1401
- self._h5group[datatype_name] = et
1487
+ # to correspond with netcdf4-python/netcdf-c we need to create
1488
+ # with low level API, to keep enums ordered by value
1489
+ # works only for h5py
1490
+ if self._root._h5py.__name__ == "h5py":
1491
+ _commit_enum_type(self, datatype_name, enum_dict, datatype)
1492
+ else:
1493
+ et = self._root._h5py.enum_dtype(enum_dict, basetype=datatype)
1494
+ self._h5group[datatype_name] = et
1402
1495
  # create enumtype class instance
1403
1496
  enumtype = self._enumtype_cls(self, datatype_name)
1404
1497
  self._enumtypes[datatype_name] = enumtype
@@ -1442,7 +1535,15 @@ class Group(Mapping):
1442
1535
 
1443
1536
 
1444
1537
  class File(Group):
1445
- def __init__(self, path, mode="r", invalid_netcdf=False, phony_dims=None, **kwargs):
1538
+ def __init__(
1539
+ self,
1540
+ path,
1541
+ mode="r",
1542
+ format="NETCDF4",
1543
+ invalid_netcdf=False,
1544
+ phony_dims=None,
1545
+ **kwargs,
1546
+ ):
1446
1547
  """NetCDF4 file constructor.
1447
1548
 
1448
1549
  Parameters
@@ -1454,6 +1555,10 @@ class File(Group):
1454
1555
  mode: "r", "r+", "a", "w"
1455
1556
  A valid file access mode. Defaults to "r".
1456
1557
 
1558
+ format: "NETCDF4", "NETCDF4_CLASSIC"
1559
+ The format of the file to create. Only relevant when creating a new
1560
+ file (mode "w"). Defaults to "NETCDF4".
1561
+
1457
1562
  invalid_netcdf: bool
1458
1563
  Allow writing netCDF4 with data types and attributes that would
1459
1564
  otherwise not generate netCDF4 files that can be read by other
@@ -1570,8 +1675,16 @@ class File(Group):
1570
1675
  else:
1571
1676
  self._closed = False
1572
1677
 
1678
+ if self._preexisting_file:
1679
+ format = (
1680
+ "NETCDF4_CLASSIC"
1681
+ if self._h5file.attrs.get("_nc3_strict")
1682
+ else "NETCDF4"
1683
+ )
1684
+
1573
1685
  self._filename = self._h5file.filename
1574
1686
  self._mode = mode
1687
+ self._format = format
1575
1688
  self._writable = mode != "r"
1576
1689
  self._root_ref = weakref.ref(self)
1577
1690
  self._h5path = "/"
@@ -1588,6 +1701,9 @@ class File(Group):
1588
1701
  "phony_dims='access' for per access naming."
1589
1702
  )
1590
1703
 
1704
+ if format not in ["NETCDF4", "NETCDF4_CLASSIC"]:
1705
+ raise ValueError(f"unknown format {format!r}")
1706
+
1591
1707
  # string decoding
1592
1708
  if "legacy" in self._cls_name:
1593
1709
  if self.decode_vlen_strings is not None:
@@ -1638,6 +1754,11 @@ class File(Group):
1638
1754
  description = "boolean"
1639
1755
  elif self._h5py.check_dtype(ref=dtype) is not None:
1640
1756
  description = "reference"
1757
+ elif (
1758
+ dtype in [int, np.int64, np.uint64, np.uint32, np.uint16, np.uint8]
1759
+ and self._format == "NETCDF4_CLASSIC"
1760
+ ):
1761
+ description = f"{dtype} (CLASSIC)"
1641
1762
  else:
1642
1763
  description = None
1643
1764
 
@@ -1659,6 +1780,10 @@ class File(Group):
1659
1780
  def parent(self):
1660
1781
  return None
1661
1782
 
1783
+ @property
1784
+ def data_model(self):
1785
+ return self._format
1786
+
1662
1787
  @property
1663
1788
  def _root(self):
1664
1789
  return self
@@ -1672,12 +1797,20 @@ class File(Group):
1672
1797
  f"hdf5={self._h5py.version.hdf5_version},"
1673
1798
  f"{self._h5py.__name__}={self._h5py.__version__}"
1674
1799
  )
1675
- self.attrs._h5attrs["_NCProperties"] = np.array(
1676
- _NC_PROPERTIES,
1677
- dtype=self._h5py.string_dtype(
1678
- encoding="ascii", length=len(_NC_PROPERTIES)
1679
- ),
1680
- )
1800
+ if self._format == "NETCDF4_CLASSIC" and self._h5py.__name__ == "h5py":
1801
+ _create_string_attribute(
1802
+ self.attrs._h5attrs._id, "_NCProperties", _NC_PROPERTIES
1803
+ )
1804
+ else:
1805
+ self.attrs._h5attrs["_NCProperties"] = np.array(
1806
+ _NC_PROPERTIES,
1807
+ dtype=self._h5py.string_dtype(
1808
+ encoding="ascii", length=len(_NC_PROPERTIES)
1809
+ ),
1810
+ )
1811
+ if self._format == "NETCDF4_CLASSIC":
1812
+ self.attrs._h5attrs["_nc3_strict"] = np.array(1, np.int32)
1813
+
1681
1814
  if self.invalid_netcdf:
1682
1815
  # see https://github.com/h5netcdf/h5netcdf/issues/165
1683
1816
  # warn user if .nc file extension is used for invalid netcdf features
@@ -1692,6 +1825,8 @@ class File(Group):
1692
1825
  # remove _NCProperties if invalid_netcdf if exists
1693
1826
  if "_NCProperties" in self.attrs._h5attrs:
1694
1827
  del self.attrs._h5attrs["_NCProperties"]
1828
+ if "_nc3_strict" in self.attrs._h5attrs:
1829
+ del self.attrs._h5attrs["_nc3_strict"]
1695
1830
 
1696
1831
  sync = flush
1697
1832