cf-xarray 0.8.7__tar.gz → 0.8.8__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.
Files changed (90) hide show
  1. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.github/workflows/pypi.yaml +5 -5
  2. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.github/workflows/testpypi-release.yaml +4 -4
  3. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.pre-commit-config.yaml +4 -4
  4. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/PKG-INFO +1 -1
  5. cf_xarray-0.8.8/cf_xarray/_version.py +1 -0
  6. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/datasets.py +10 -0
  7. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/formatting.py +55 -9
  8. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/geometry.py +179 -33
  9. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/test_accessor.py +27 -0
  10. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/test_geometry.py +239 -15
  11. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray.egg-info/PKG-INFO +2 -2
  12. cf_xarray-0.8.8/codecov.yml +3 -0
  13. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/whats-new.rst +6 -0
  14. cf_xarray-0.8.7/cf_xarray/_version.py +0 -1
  15. cf_xarray-0.8.7/codecov.yml +0 -2
  16. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.binder/environment.yml +0 -0
  17. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.deepsource.toml +0 -0
  18. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.github/dependabot.yml +0 -0
  19. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.github/workflows/ci.yaml +0 -0
  20. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.github/workflows/parse_logs.py +0 -0
  21. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.github/workflows/upstream-dev-ci.yaml +0 -0
  22. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.gitignore +0 -0
  23. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.readthedocs.yml +0 -0
  24. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/.tributors +0 -0
  25. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/CITATION.cff +0 -0
  26. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/LICENSE +0 -0
  27. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/README.rst +0 -0
  28. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/__init__.py +0 -0
  29. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/accessor.py +0 -0
  30. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/coding.py +0 -0
  31. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/criteria.py +0 -0
  32. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/helpers.py +0 -0
  33. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/options.py +0 -0
  34. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/py.typed +0 -0
  35. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/scripts/make_doc.py +0 -0
  36. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/scripts/print_versions.py +0 -0
  37. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/sgrid.py +0 -0
  38. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/__init__.py +0 -0
  39. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/test_coding.py +0 -0
  40. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/test_helpers.py +0 -0
  41. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/test_options.py +0 -0
  42. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/test_scripts.py +0 -0
  43. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/tests/test_units.py +0 -0
  44. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/units.py +0 -0
  45. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray/utils.py +0 -0
  46. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray.egg-info/SOURCES.txt +0 -0
  47. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray.egg-info/dependency_links.txt +0 -0
  48. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray.egg-info/requires.txt +0 -0
  49. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/cf_xarray.egg-info/top_level.txt +0 -0
  50. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/ci/doc.yml +0 -0
  51. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/ci/environment-no-optional-deps.yml +0 -0
  52. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/ci/environment.yml +0 -0
  53. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/ci/upstream-dev-env.yml +0 -0
  54. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/2D_bounds_averaged.png +0 -0
  55. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/2D_bounds_error.png +0 -0
  56. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/2D_bounds_nonunique.png +0 -0
  57. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/Makefile +0 -0
  58. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/_static/dataset-diagram-logo.tex +0 -0
  59. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/_static/full-logo.png +0 -0
  60. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/_static/logo.png +0 -0
  61. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/_static/logo.svg +0 -0
  62. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/_static/rich-repr-example.png +0 -0
  63. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/_static/style.css +0 -0
  64. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/api.rst +0 -0
  65. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/bounds.md +0 -0
  66. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/cartopy_rotated_pole.png +0 -0
  67. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/coding.md +0 -0
  68. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/conf.py +0 -0
  69. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/contributing.rst +0 -0
  70. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/coord_axes.md +0 -0
  71. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/custom-criteria.md +0 -0
  72. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/dsg.md +0 -0
  73. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/examples/introduction.ipynb +0 -0
  74. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/faq.md +0 -0
  75. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/flags.md +0 -0
  76. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/geometry.md +0 -0
  77. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/grid_mappings.md +0 -0
  78. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/howtouse.md +0 -0
  79. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/index.rst +0 -0
  80. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/make.bat +0 -0
  81. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/parametricz.md +0 -0
  82. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/plotting.md +0 -0
  83. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/provenance.md +0 -0
  84. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/quickstart.md +0 -0
  85. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/roadmap.rst +0 -0
  86. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/selecting.md +0 -0
  87. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/sgrid_ugrid.md +0 -0
  88. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/doc/units.md +0 -0
  89. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/pyproject.toml +0 -0
  90. {cf_xarray-0.8.7 → cf_xarray-0.8.8}/setup.cfg +0 -0
@@ -15,7 +15,7 @@ jobs:
15
15
  - uses: actions/checkout@v4
16
16
  with:
17
17
  fetch-depth: 0
18
- - uses: actions/setup-python@v4
18
+ - uses: actions/setup-python@v5
19
19
  name: Install Python
20
20
  with:
21
21
  python-version: "3.10"
@@ -41,7 +41,7 @@ jobs:
41
41
  else
42
42
  echo "✅ Looks good"
43
43
  fi
44
- - uses: actions/upload-artifact@v3
44
+ - uses: actions/upload-artifact@v4
45
45
  with:
46
46
  name: releases
47
47
  path: dist
@@ -50,11 +50,11 @@ jobs:
50
50
  needs: build-artifacts
51
51
  runs-on: ubuntu-latest
52
52
  steps:
53
- - uses: actions/setup-python@v4
53
+ - uses: actions/setup-python@v5
54
54
  name: Install Python
55
55
  with:
56
56
  python-version: "3.10"
57
- - uses: actions/download-artifact@v3
57
+ - uses: actions/download-artifact@v4
58
58
  with:
59
59
  name: releases
60
60
  path: dist
@@ -91,7 +91,7 @@ jobs:
91
91
  id-token: write
92
92
 
93
93
  steps:
94
- - uses: actions/download-artifact@v3
94
+ - uses: actions/download-artifact@v4
95
95
  with:
96
96
  name: releases
97
97
  path: dist
@@ -21,7 +21,7 @@ jobs:
21
21
  with:
22
22
  fetch-depth: 0
23
23
 
24
- - uses: actions/setup-python@v4
24
+ - uses: actions/setup-python@v5
25
25
  name: Install Python
26
26
  with:
27
27
  python-version: "3.10"
@@ -53,7 +53,7 @@ jobs:
53
53
  echo "✅ Looks good"
54
54
  fi
55
55
 
56
- - uses: actions/upload-artifact@v3
56
+ - uses: actions/upload-artifact@v4
57
57
  with:
58
58
  name: releases
59
59
  path: dist
@@ -62,11 +62,11 @@ jobs:
62
62
  needs: build-artifacts
63
63
  runs-on: ubuntu-latest
64
64
  steps:
65
- - uses: actions/setup-python@v4
65
+ - uses: actions/setup-python@v5
66
66
  name: Install Python
67
67
  with:
68
68
  python-version: "3.10"
69
- - uses: actions/download-artifact@v3
69
+ - uses: actions/download-artifact@v4
70
70
  with:
71
71
  name: releases
72
72
  path: dist
@@ -10,13 +10,13 @@ repos:
10
10
 
11
11
  - repo: https://github.com/astral-sh/ruff-pre-commit
12
12
  # Ruff version.
13
- rev: 'v0.1.1'
13
+ rev: 'v0.1.9'
14
14
  hooks:
15
15
  - id: ruff
16
16
  args: ["--show-fixes", "--fix"]
17
17
 
18
18
  - repo: https://github.com/psf/black-pre-commit-mirror
19
- rev: 23.10.1
19
+ rev: 23.12.1
20
20
  hooks:
21
21
  - id: black
22
22
 
@@ -36,7 +36,7 @@ repos:
36
36
  - mdformat-myst
37
37
 
38
38
  - repo: https://github.com/nbQA-dev/nbQA
39
- rev: 1.7.0
39
+ rev: 1.7.1
40
40
  hooks:
41
41
  - id: nbqa-black
42
42
  - id: nbqa-ruff
@@ -56,7 +56,7 @@ repos:
56
56
  - id: debug-statements
57
57
 
58
58
  - repo: https://github.com/keewis/blackdoc
59
- rev: v0.3.8
59
+ rev: v0.3.9
60
60
  hooks:
61
61
  - id: blackdoc
62
62
  files: .+\.py$
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cf_xarray
3
- Version: 0.8.7
3
+ Version: 0.8.8
4
4
  Summary: A convenience wrapper for using CF attributes on xarray objects
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -0,0 +1 @@
1
+ __version__ = "0.8.8"
@@ -503,6 +503,16 @@ flag_indep = xr.DataArray(
503
503
  name="flag_var",
504
504
  )
505
505
 
506
+ flag_indep_uint16 = xr.DataArray(
507
+ np.array([1, 10, 100, 1000, 10000, 65535], dtype=np.uint16),
508
+ dims=("time",),
509
+ attrs={
510
+ "flag_masks": [2**i for i in range(16)],
511
+ "flag_meanings": " ".join([f"flag_{2**i}" for i in range(16)]),
512
+ "standard_name": "flag_independent",
513
+ },
514
+ name="flag_var",
515
+ )
506
516
 
507
517
  flag_mix = xr.DataArray(
508
518
  np.array([4, 8, 13, 5, 10, 14, 7, 3], np.uint8),
@@ -151,8 +151,47 @@ def _maybe_panel(textgen, title: str, rich: bool):
151
151
  return title + ":\n" + text
152
152
 
153
153
 
154
- def find_set_bits(mask, value, repeated_masks):
155
- bitpos = np.arange(8)[::-1]
154
+ def _get_bit_length(dtype):
155
+ # Check if dtype is a numpy dtype, if not, convert it
156
+ if not isinstance(dtype, np.dtype):
157
+ dtype = np.dtype(dtype)
158
+
159
+ # Calculate the bit length
160
+ bit_length = 8 * dtype.itemsize
161
+
162
+ return bit_length
163
+
164
+
165
+ def _unpackbits(mask, bit_length):
166
+ # Ensure the array is a numpy array
167
+ arr = np.asarray(mask)
168
+
169
+ # Create an output array of the appropriate shape
170
+ output_shape = arr.shape + (bit_length,)
171
+ output = np.zeros(output_shape, dtype=np.uint8)
172
+
173
+ # Unpack bits
174
+ for i in range(bit_length):
175
+ output[..., i] = (arr >> i) & 1
176
+
177
+ return output[..., ::-1]
178
+
179
+
180
+ def _max_chars_for_bit_length(bit_length):
181
+ """
182
+ Find the maximum characters needed for a fixed-width display
183
+ for integer values of a certain bit_length. Use calculation
184
+ for signed integers, since it conservatively will always have
185
+ enough characters for signed or unsigned.
186
+ """
187
+ # Maximum value for signed integers of this bit length
188
+ max_val = 2 ** (bit_length - 1) - 1
189
+ # Add 1 for the negative sign
190
+ return len(str(max_val)) + 1
191
+
192
+
193
+ def find_set_bits(mask, value, repeated_masks, bit_length):
194
+ bitpos = np.arange(bit_length)[::-1]
156
195
  if mask not in repeated_masks:
157
196
  if value == 0:
158
197
  return [-1]
@@ -161,8 +200,8 @@ def find_set_bits(mask, value, repeated_masks):
161
200
  else:
162
201
  return [int(np.log2(mask))]
163
202
  else:
164
- allset = bitpos[np.unpackbits(np.uint8(mask)) == 1]
165
- setbits = bitpos[np.unpackbits(np.uint8(mask & value)) == 1]
203
+ allset = bitpos[_unpackbits(mask, bit_length) == 1]
204
+ setbits = bitpos[_unpackbits(mask & value, bit_length) == 1]
166
205
  return [b if abs(b) in setbits else -b for b in allset]
167
206
 
168
207
 
@@ -184,6 +223,11 @@ def _format_flags(accessor, rich):
184
223
  # for f, (m, _) in flag_dict.items()
185
224
  # if m is not None and m not in repeated_masks
186
225
  # ]
226
+
227
+ bit_length = _get_bit_length(accessor._obj.dtype)
228
+ mask_width = _max_chars_for_bit_length(bit_length)
229
+ key_width = max(len(key) for key in flag_dict)
230
+
187
231
  bit_text = []
188
232
  value_text = []
189
233
  for key, (mask, value) in flag_dict.items():
@@ -191,8 +235,8 @@ def _format_flags(accessor, rich):
191
235
  bit_text.append("✗" if rich else "")
192
236
  value_text.append(str(value))
193
237
  continue
194
- bits = find_set_bits(mask, value, repeated_masks)
195
- bitstring = ["."] * 8
238
+ bits = find_set_bits(mask, value, repeated_masks, bit_length)
239
+ bitstring = ["."] * bit_length
196
240
  if bits == [-1]:
197
241
  continue
198
242
  else:
@@ -200,9 +244,9 @@ def _format_flags(accessor, rich):
200
244
  bitstring[abs(b)] = _format_cf_name("1" if b >= 0 else "0", rich)
201
245
  text = "".join(bitstring[::-1])
202
246
  value_text.append(
203
- f"{mask} & {value}"
247
+ f"{mask:{mask_width}} & {value}"
204
248
  if key in excl_flags and value is not None
205
- else str(mask)
249
+ else f"{mask:{mask_width}}"
206
250
  )
207
251
  bit_text.append(text if rich else f" / Bit: {text}")
208
252
 
@@ -230,7 +274,9 @@ def _format_flags(accessor, rich):
230
274
  else:
231
275
  rows = []
232
276
  for val, bit, key in zip(value_text, bit_text, flag_dict):
233
- rows.append(f"{TAB}{_format_cf_name(key, rich)}: {TAB} {val} {bit}")
277
+ rows.append(
278
+ f"{TAB}{_format_cf_name(key, rich):>{key_width}}: {TAB} {val} {bit}"
279
+ )
234
280
  return _print_rows("Flag Meanings", rows, rich)
235
281
 
236
282
 
@@ -75,9 +75,6 @@ def shapely_to_cf(geometries: xr.DataArray | Sequence, grid_mapping: str | None
75
75
  """
76
76
  Convert a DataArray with shapely geometry objects into a CF-compliant dataset.
77
77
 
78
- .. warning::
79
- Only point geometries are currently implemented.
80
-
81
78
  Parameters
82
79
  ----------
83
80
  geometries : sequence of shapely geometries or xarray.DataArray
@@ -115,7 +112,7 @@ def shapely_to_cf(geometries: xr.DataArray | Sequence, grid_mapping: str | None
115
112
  elif types.issubset({"LineString", "MultiLineString"}):
116
113
  ds = lines_to_cf(geometries)
117
114
  elif types.issubset({"Polygon", "MultiPolygon"}):
118
- raise NotImplementedError("Polygon geometry conversion is not implemented.")
115
+ ds = polygons_to_cf(geometries)
119
116
  else:
120
117
  raise ValueError(
121
118
  f"Mixed geometry types are not supported in CF-compliant datasets. Got {types}"
@@ -142,9 +139,6 @@ def cf_to_shapely(ds: xr.Dataset):
142
139
  """
143
140
  Convert geometries stored in a CF-compliant way to shapely objects stored in a single variable.
144
141
 
145
- .. warning::
146
- Only point geometries are currently implemented.
147
-
148
142
  Parameters
149
143
  ----------
150
144
  ds : xr.Dataset
@@ -168,7 +162,7 @@ def cf_to_shapely(ds: xr.Dataset):
168
162
  elif geom_type == "line":
169
163
  geometries = cf_to_lines(ds)
170
164
  elif geom_type == "polygon":
171
- raise NotImplementedError("Polygon geometry conversion is not implemented.")
165
+ geometries = cf_to_polygons(ds)
172
166
  else:
173
167
  raise ValueError(
174
168
  f"Valid CF geometry types are 'point', 'line' and 'polygon'. Got {geom_type}"
@@ -328,13 +322,13 @@ def lines_to_cf(lines: xr.DataArray | Sequence):
328
322
  x = arr[:, 0]
329
323
  y = arr[:, 1]
330
324
 
331
- node_count = np.diff(offsets[0])
325
+ part_node_count = np.diff(offsets[0])
332
326
  if len(offsets) == 1:
333
327
  indices = offsets[0]
334
- part_node_count = node_count
328
+ node_count = part_node_count
335
329
  else:
336
330
  indices = np.take(offsets[0], offsets[1])
337
- part_node_count = np.diff(indices)
331
+ node_count = np.diff(indices)
338
332
 
339
333
  geom_coords = arr.take(indices[:-1], 0)
340
334
  crdX = geom_coords[:, 0]
@@ -342,8 +336,8 @@ def lines_to_cf(lines: xr.DataArray | Sequence):
342
336
 
343
337
  ds = xr.Dataset(
344
338
  data_vars={
345
- "node_count": xr.DataArray(node_count, dims=("segment",)),
346
- "part_node_count": xr.DataArray(part_node_count, dims=(dim,)),
339
+ "node_count": xr.DataArray(node_count, dims=(dim,)),
340
+ "part_node_count": xr.DataArray(part_node_count, dims=("part",)),
347
341
  "geometry_container": xr.DataArray(
348
342
  attrs={
349
343
  "geometry_type": "line",
@@ -394,7 +388,7 @@ def cf_to_lines(ds: xr.Dataset):
394
388
  # Shorthand for convenience
395
389
  geo = ds.geometry_container.attrs
396
390
 
397
- # The features dimension name, defaults to the one of 'part_node_count'
391
+ # The features dimension name, defaults to the one of 'node_count'
398
392
  # or the dimension of the coordinates, if present.
399
393
  feat_dim = None
400
394
  if "coordinates" in geo:
@@ -405,36 +399,188 @@ def cf_to_lines(ds: xr.Dataset):
405
399
  xy = np.stack([ds[x_name].values, ds[y_name].values], axis=-1)
406
400
 
407
401
  node_count_name = geo.get("node_count")
408
- part_node_count_name = geo.get("part_node_count")
402
+ part_node_count_name = geo.get("part_node_count", node_count_name)
409
403
  if node_count_name is None:
410
404
  raise ValueError("'node_count' must be provided for line geometries")
411
405
  else:
412
406
  node_count = ds[node_count_name]
407
+ feat_dim = feat_dim or "index"
408
+ if feat_dim in ds.coords:
409
+ node_count = node_count.assign_coords({feat_dim: ds[feat_dim]})
413
410
 
414
- offset1 = np.insert(np.cumsum(node_count.values), 0, 0)
411
+ # first get geometries for all the parts
412
+ part_node_count = ds[part_node_count_name]
413
+ offset1 = np.insert(np.cumsum(part_node_count.values), 0, 0)
415
414
  lines = from_ragged_array(GeometryType.LINESTRING, xy, offsets=(offset1,))
416
415
 
417
- if part_node_count_name is None:
418
- # No part_node_count means there are no multilines
419
- # And if we had no coordinates, then the dimension defaults to "index"
416
+ # get index of offset2 values that are edges for part_node_count
417
+ offset2 = np.nonzero(np.isin(offset1, np.insert(np.cumsum(node_count), 0, 0)))[0]
418
+
419
+ multilines = from_ragged_array(
420
+ GeometryType.MULTILINESTRING, xy, offsets=(offset1, offset2)
421
+ )
422
+
423
+ # get items from lines or multilines depending on number of parts
424
+ geoms = np.where(np.diff(offset2) == 1, lines[offset2[:-1]], multilines)
425
+
426
+ return xr.DataArray(geoms, dims=node_count.dims, coords=node_count.coords)
427
+
428
+
429
+ def polygons_to_cf(polygons: xr.DataArray | Sequence):
430
+ """Convert an iterable of polygons (shapely.geometry.[Multi]Polygon) into a CF-compliant geometry dataset.
431
+
432
+ Parameters
433
+ ----------
434
+ polygons : sequence of shapely.geometry.Polygon or MultiPolygon
435
+ The sequence of [multi]polygons to translate to a CF dataset.
436
+
437
+ Returns
438
+ -------
439
+ xr.Dataset
440
+ A Dataset with variables 'x', 'y', 'crd_x', 'crd_y', 'node_count' and 'geometry_container'
441
+ and optionally 'part_node_count'.
442
+ """
443
+ from shapely import to_ragged_array
444
+
445
+ if isinstance(polygons, xr.DataArray):
446
+ dim = polygons.dims[0]
447
+ coord = polygons[dim] if dim in polygons.coords else None
448
+ polygons_ = polygons.values
449
+ else:
450
+ dim = "index"
451
+ coord = None
452
+ polygons_ = np.array(polygons)
453
+
454
+ _, arr, offsets = to_ragged_array(polygons_)
455
+ x = arr[:, 0]
456
+ y = arr[:, 1]
457
+
458
+ part_node_count = np.diff(offsets[0])
459
+ if len(offsets) == 1:
460
+ indices = offsets[0]
461
+ node_count = part_node_count
462
+ elif len(offsets) >= 2:
463
+ indices = np.take(offsets[0], offsets[1])
464
+ interior_ring = np.isin(offsets[0], indices, invert=True)[:-1].astype(int)
465
+
466
+ if len(offsets) == 3:
467
+ indices = np.take(indices, offsets[2])
468
+
469
+ node_count = np.diff(indices)
470
+
471
+ geom_coords = arr.take(indices[:-1], 0)
472
+ crdX = geom_coords[:, 0]
473
+ crdY = geom_coords[:, 1]
474
+
475
+ ds = xr.Dataset(
476
+ data_vars={
477
+ "node_count": xr.DataArray(node_count, dims=(dim,)),
478
+ "interior_ring": xr.DataArray(interior_ring, dims=("part",)),
479
+ "part_node_count": xr.DataArray(part_node_count, dims=("part",)),
480
+ "geometry_container": xr.DataArray(
481
+ attrs={
482
+ "geometry_type": "polygon",
483
+ "node_count": "node_count",
484
+ "part_node_count": "part_node_count",
485
+ "interior_ring": "interior_ring",
486
+ "node_coordinates": "x y",
487
+ "coordinates": "crd_x crd_y",
488
+ }
489
+ ),
490
+ },
491
+ coords={
492
+ "x": xr.DataArray(x, dims=("node",), attrs={"axis": "X"}),
493
+ "y": xr.DataArray(y, dims=("node",), attrs={"axis": "Y"}),
494
+ "crd_x": xr.DataArray(crdX, dims=(dim,), attrs={"nodes": "x"}),
495
+ "crd_y": xr.DataArray(crdY, dims=(dim,), attrs={"nodes": "y"}),
496
+ },
497
+ )
498
+
499
+ if coord is not None:
500
+ ds = ds.assign_coords({dim: coord})
501
+
502
+ # Special case when we have no MultiPolygons and no holes
503
+ if len(ds.part_node_count) == len(ds.node_count):
504
+ ds = ds.drop_vars("part_node_count")
505
+ del ds.geometry_container.attrs["part_node_count"]
506
+
507
+ # Special case when we have no holes
508
+ if (ds.interior_ring == 0).all():
509
+ ds = ds.drop_vars("interior_ring")
510
+ del ds.geometry_container.attrs["interior_ring"]
511
+ return ds
512
+
513
+
514
+ def cf_to_polygons(ds: xr.Dataset):
515
+ """Convert polygon geometries stored in a CF-compliant way to shapely polygons stored in a single variable.
516
+
517
+ Parameters
518
+ ----------
519
+ ds : xr.Dataset
520
+ A dataset with CF-compliant polygon geometries.
521
+ Must have a "geometry_container" variable with at least a 'node_coordinates' attribute.
522
+ Must also have the two 1D variables listed by this attribute.
523
+
524
+ Returns
525
+ -------
526
+ geometry : xr.DataArray
527
+ A 1D array of shapely.geometry.[Multi]Polygon objects.
528
+ It has the same dimension as the ``part_node_count`` or the coordinates variables, or
529
+ ``'features'`` if those were not present in ``ds``.
530
+ """
531
+ from shapely import GeometryType, from_ragged_array
532
+
533
+ # Shorthand for convenience
534
+ geo = ds.geometry_container.attrs
535
+
536
+ # The features dimension name, defaults to the one of 'part_node_count'
537
+ # or the dimension of the coordinates, if present.
538
+ feat_dim = None
539
+ if "coordinates" in geo:
540
+ xcoord_name, _ = geo["coordinates"].split(" ")
541
+ (feat_dim,) = ds[xcoord_name].dims
542
+
543
+ x_name, y_name = geo["node_coordinates"].split(" ")
544
+ xy = np.stack([ds[x_name].values, ds[y_name].values], axis=-1)
545
+
546
+ node_count_name = geo.get("node_count")
547
+ part_node_count_name = geo.get("part_node_count", node_count_name)
548
+ interior_ring_name = geo.get("interior_ring")
549
+
550
+ if node_count_name is None:
551
+ raise ValueError("'node_count' must be provided for polygon geometries")
552
+ else:
553
+ node_count = ds[node_count_name]
420
554
  feat_dim = feat_dim or "index"
421
- part_node_count = xr.DataArray(node_count.values, dims=(feat_dim,))
422
555
  if feat_dim in ds.coords:
423
- part_node_count = part_node_count.assign_coords({feat_dim: ds[feat_dim]})
556
+ node_count = node_count.assign_coords({feat_dim: ds[feat_dim]})
424
557
 
425
- geoms = lines
558
+ # first get geometries for all the rings
559
+ part_node_count = ds[part_node_count_name]
560
+ offset1 = np.insert(np.cumsum(part_node_count.values), 0, 0)
561
+
562
+ if interior_ring_name is None:
563
+ offset2 = np.array(list(range(len(offset1))))
426
564
  else:
427
- part_node_count = ds[part_node_count_name]
428
-
429
- # get index of offset1 values that are edges for part_node_count
430
- offset2 = np.nonzero(
431
- np.isin(offset1, np.insert(np.cumsum(part_node_count), 0, 0))
432
- )[0]
433
- multilines = from_ragged_array(
434
- GeometryType.MULTILINESTRING, xy, offsets=(offset1, offset2)
565
+ interior_ring = ds[interior_ring_name]
566
+ if not interior_ring[0] == 0:
567
+ raise ValueError("coordinate array must start with an exterior ring")
568
+ offset2 = np.append(np.where(interior_ring == 0)[0], [len(part_node_count)])
569
+
570
+ polygons = from_ragged_array(GeometryType.POLYGON, xy, offsets=(offset1, offset2))
571
+
572
+ # get index of offset2 values that are edges for node_count
573
+ offset3 = np.nonzero(
574
+ np.isin(
575
+ offset2,
576
+ np.nonzero(np.isin(offset1, np.insert(np.cumsum(node_count), 0, 0)))[0],
435
577
  )
578
+ )[0]
579
+ multipolygons = from_ragged_array(
580
+ GeometryType.MULTIPOLYGON, xy, offsets=(offset1, offset2, offset3)
581
+ )
436
582
 
437
- # get items from lines or multilines depending on number of segments
438
- geoms = np.where(np.diff(offset2) == 1, lines[offset2[:-1]], multilines)
583
+ # get items from polygons or multipolygons depending on number of parts
584
+ geoms = np.where(np.diff(offset3) == 1, polygons[offset3[:-1]], multipolygons)
439
585
 
440
- return xr.DataArray(geoms, dims=part_node_count.dims, coords=part_node_count.coords)
586
+ return xr.DataArray(geoms, dims=node_count.dims, coords=node_count.coords)
@@ -26,6 +26,7 @@ from ..datasets import (
26
26
  dsg,
27
27
  flag_excl,
28
28
  flag_indep,
29
+ flag_indep_uint16,
29
30
  flag_mix,
30
31
  forecast,
31
32
  mollwds,
@@ -164,6 +165,7 @@ def test_repr() -> None:
164
165
  # Flag DataArray
165
166
  assert "Flag Variable" in repr(flag_excl.cf)
166
167
  assert "Flag Variable" in repr(flag_indep.cf)
168
+ assert "Flag Variable" in repr(flag_indep_uint16.cf)
167
169
  assert "Flag Variable" in repr(flag_mix.cf)
168
170
  assert "Flag Variable" in repr(basin.cf)
169
171
 
@@ -1837,6 +1839,30 @@ class TestFlags:
1837
1839
  res = flag_indep.cf.flags[name]
1838
1840
  np.testing.assert_equal(res.to_numpy(), expected[i])
1839
1841
 
1842
+ def test_flag_indep_uint16(self) -> None:
1843
+ expected = [
1844
+ [True, False, False, False, False, True], # bit 1
1845
+ [False, True, False, False, False, True], # bit 2
1846
+ [False, False, True, False, False, True], # bit 4
1847
+ [False, True, False, True, False, True], # bit 8
1848
+ [False, False, False, False, True, True], # bit 16
1849
+ [False, False, True, True, False, True], # bit 32
1850
+ [False, False, True, True, False, True], # bit 64
1851
+ [False, False, False, True, False, True], # bit 128
1852
+ [False, False, False, True, True, True], # bit 256
1853
+ [False, False, False, True, True, True], # bit 512
1854
+ [False, False, False, False, True, True], # bit 1024
1855
+ [False, False, False, False, False, True], # bit 2048
1856
+ [False, False, False, False, False, True], # bit 4096
1857
+ [False, False, False, False, True, True], # bit 8192
1858
+ [False, False, False, False, False, True], # bit 16384
1859
+ [False, False, False, False, False, True], # bit 32768
1860
+ ]
1861
+ for i in range(16):
1862
+ name = f"flag_{2**i}"
1863
+ res = flag_indep_uint16.cf.flags[name]
1864
+ np.testing.assert_equal(res.to_numpy(), expected[i])
1865
+
1840
1866
  def test_flag_mix(self) -> None:
1841
1867
  expected = [
1842
1868
  [False, False, True, True, False, False, True, True], # flag 1
@@ -1983,6 +2009,7 @@ def test_curvefit() -> None:
1983
2009
  [basin, "Flag Variable"],
1984
2010
  [flag_mix, "Flag Variable"],
1985
2011
  [flag_indep, "Flag Variable"],
2012
+ [flag_indep_uint16, "Flag Variable"],
1986
2013
  [flag_excl, "Flag Variable"],
1987
2014
  [dsg, "Discrete Sampling Geometry"],
1988
2015
  ),
@@ -71,8 +71,8 @@ def geometry_line_ds():
71
71
  y=xr.DataArray(
72
72
  [0, 2, 4, 6, 0, 0, 1, 1.0, 2.0, 9.5], dims=("node",), attrs={"axis": "Y"}
73
73
  ),
74
- part_node_count=xr.DataArray([4, 3, 3], dims=("index",)),
75
- node_count=xr.DataArray([2, 2, 3, 3], dims=("segment",)),
74
+ node_count=xr.DataArray([4, 3, 3], dims=("index",)),
75
+ part_node_count=xr.DataArray([2, 2, 3, 3], dims=("part",)),
76
76
  crd_x=xr.DataArray([0.0, 0.0, 1.0], dims=("index",), attrs={"nodes": "x"}),
77
77
  crd_y=xr.DataArray([0.0, 0.0, 1.0], dims=("index",), attrs={"nodes": "y"}),
78
78
  geometry_container=xr.DataArray(
@@ -108,7 +108,7 @@ def geometry_line_without_multilines_ds():
108
108
  cf_ds = ds.assign(
109
109
  x=xr.DataArray([0, 1, 1, 1.0, 2.0, 1.7], dims=("node",), attrs={"axis": "X"}),
110
110
  y=xr.DataArray([0, 0, 1, 1.0, 2.0, 9.5], dims=("node",), attrs={"axis": "Y"}),
111
- node_count=xr.DataArray([3, 3], dims=("segment",)),
111
+ node_count=xr.DataArray([3, 3], dims=("index",)),
112
112
  crd_x=xr.DataArray([0.0, 1.0], dims=("index",), attrs={"nodes": "x"}),
113
113
  crd_y=xr.DataArray([0.0, 1.0], dims=("index",), attrs={"nodes": "y"}),
114
114
  geometry_container=xr.DataArray(
@@ -126,6 +126,154 @@ def geometry_line_without_multilines_ds():
126
126
  return cf_ds, shp_da
127
127
 
128
128
 
129
+ @pytest.fixture
130
+ def geometry_polygon_without_holes_ds():
131
+ from shapely.geometry import Polygon
132
+
133
+ # empty/fill workaround to avoid numpy deprecation(warning) due to the array interface of shapely geometries.
134
+ geoms = np.empty(2, dtype=object)
135
+ geoms[:] = [
136
+ Polygon(([50, 0], [40, 15], [30, 0])),
137
+ Polygon(([70, 50], [60, 65], [50, 50])),
138
+ ]
139
+
140
+ ds = xr.Dataset()
141
+ shp_da = xr.DataArray(geoms, dims=("index",), name="geometry")
142
+
143
+ cf_ds = ds.assign(
144
+ x=xr.DataArray(
145
+ [50, 40, 30, 50, 70, 60, 50, 70], dims=("node",), attrs={"axis": "X"}
146
+ ),
147
+ y=xr.DataArray(
148
+ [0, 15, 0, 0, 50, 65, 50, 50], dims=("node",), attrs={"axis": "Y"}
149
+ ),
150
+ node_count=xr.DataArray([4, 4], dims=("index",)),
151
+ crd_x=xr.DataArray([50, 70], dims=("index",), attrs={"nodes": "x"}),
152
+ crd_y=xr.DataArray([0, 50], dims=("index",), attrs={"nodes": "y"}),
153
+ geometry_container=xr.DataArray(
154
+ attrs={
155
+ "geometry_type": "polygon",
156
+ "node_count": "node_count",
157
+ "node_coordinates": "x y",
158
+ "coordinates": "crd_x crd_y",
159
+ }
160
+ ),
161
+ )
162
+
163
+ cf_ds = cf_ds.set_coords(["x", "y", "crd_x", "crd_y"])
164
+
165
+ return cf_ds, shp_da
166
+
167
+
168
+ @pytest.fixture
169
+ def geometry_polygon_without_multipolygons_ds():
170
+ from shapely.geometry import Polygon
171
+
172
+ # empty/fill workaround to avoid numpy deprecation(warning) due to the array interface of shapely geometries.
173
+ geoms = np.empty(2, dtype=object)
174
+ geoms[:] = [
175
+ Polygon(([50, 0], [40, 15], [30, 0])),
176
+ Polygon(
177
+ ([70, 50], [60, 65], [50, 50]),
178
+ [
179
+ ([55, 55], [60, 60], [65, 55]),
180
+ ],
181
+ ),
182
+ ]
183
+
184
+ ds = xr.Dataset()
185
+ shp_da = xr.DataArray(geoms, dims=("index",), name="geometry")
186
+
187
+ cf_ds = ds.assign(
188
+ x=xr.DataArray(
189
+ [50, 40, 30, 50, 70, 60, 50, 70, 55, 60, 65, 55],
190
+ dims=("node",),
191
+ attrs={"axis": "X"},
192
+ ),
193
+ y=xr.DataArray(
194
+ [0, 15, 0, 0, 50, 65, 50, 50, 55, 60, 55, 55],
195
+ dims=("node",),
196
+ attrs={"axis": "Y"},
197
+ ),
198
+ node_count=xr.DataArray([4, 8], dims=("index",)),
199
+ part_node_count=xr.DataArray([4, 4, 4], dims=("part",)),
200
+ interior_ring=xr.DataArray([0, 0, 1], dims=("part",)),
201
+ crd_x=xr.DataArray([50, 70], dims=("index",), attrs={"nodes": "x"}),
202
+ crd_y=xr.DataArray([0, 50], dims=("index",), attrs={"nodes": "y"}),
203
+ geometry_container=xr.DataArray(
204
+ attrs={
205
+ "geometry_type": "polygon",
206
+ "node_count": "node_count",
207
+ "part_node_count": "part_node_count",
208
+ "interior_ring": "interior_ring",
209
+ "node_coordinates": "x y",
210
+ "coordinates": "crd_x crd_y",
211
+ }
212
+ ),
213
+ )
214
+
215
+ cf_ds = cf_ds.set_coords(["x", "y", "crd_x", "crd_y"])
216
+
217
+ return cf_ds, shp_da
218
+
219
+
220
+ @pytest.fixture
221
+ def geometry_polygon_ds():
222
+ from shapely.geometry import MultiPolygon, Polygon
223
+
224
+ # empty/fill workaround to avoid numpy deprecation(warning) due to the array interface of shapely geometries.
225
+ geoms = np.empty(2, dtype=object)
226
+ geoms[:] = [
227
+ MultiPolygon(
228
+ [
229
+ (
230
+ ([20, 0], [10, 15], [0, 0]),
231
+ [
232
+ ([5, 5], [10, 10], [15, 5]),
233
+ ],
234
+ ),
235
+ (([20, 20], [10, 35], [0, 20]),),
236
+ ]
237
+ ),
238
+ Polygon(([50, 0], [40, 15], [30, 0])),
239
+ ]
240
+
241
+ ds = xr.Dataset()
242
+ shp_da = xr.DataArray(geoms, dims=("index",), name="geometry")
243
+
244
+ cf_ds = ds.assign(
245
+ x=xr.DataArray(
246
+ [20, 10, 0, 20, 5, 10, 15, 5, 20, 10, 0, 20, 50, 40, 30, 50],
247
+ dims=("node",),
248
+ attrs={"axis": "X"},
249
+ ),
250
+ y=xr.DataArray(
251
+ [0, 15, 0, 0, 5, 10, 5, 5, 20, 35, 20, 20, 0, 15, 0, 0],
252
+ dims=("node",),
253
+ attrs={"axis": "Y"},
254
+ ),
255
+ node_count=xr.DataArray([12, 4], dims=("index",)),
256
+ part_node_count=xr.DataArray([4, 4, 4, 4], dims=("part",)),
257
+ interior_ring=xr.DataArray([0, 1, 0, 0], dims=("part",)),
258
+ crd_x=xr.DataArray([20, 50], dims=("index",), attrs={"nodes": "x"}),
259
+ crd_y=xr.DataArray([0, 0], dims=("index",), attrs={"nodes": "y"}),
260
+ geometry_container=xr.DataArray(
261
+ attrs={
262
+ "geometry_type": "polygon",
263
+ "node_count": "node_count",
264
+ "part_node_count": "part_node_count",
265
+ "interior_ring": "interior_ring",
266
+ "node_coordinates": "x y",
267
+ "coordinates": "crd_x crd_y",
268
+ }
269
+ ),
270
+ )
271
+
272
+ cf_ds = cf_ds.set_coords(["x", "y", "crd_x", "crd_y"])
273
+
274
+ return cf_ds, shp_da
275
+
276
+
129
277
  @requires_shapely
130
278
  def test_shapely_to_cf(geometry_ds):
131
279
  from shapely.geometry import Point
@@ -192,6 +340,43 @@ def test_shapely_to_cf_for_lines_without_multilines(
192
340
  xr.testing.assert_identical(actual, expected)
193
341
 
194
342
 
343
+ @requires_shapely
344
+ def test_shapely_to_cf_for_polygons_as_da(geometry_polygon_ds):
345
+ expected, in_da = geometry_polygon_ds
346
+
347
+ actual = cfxr.shapely_to_cf(in_da)
348
+ xr.testing.assert_identical(actual, expected)
349
+
350
+ in_da = in_da.assign_coords(index=["a", "b"])
351
+ actual = cfxr.shapely_to_cf(in_da)
352
+ xr.testing.assert_identical(actual, expected.assign_coords(index=["a", "b"]))
353
+
354
+
355
+ @requires_shapely
356
+ def test_shapely_to_cf_for_polygons_as_sequence(geometry_polygon_ds):
357
+ expected, in_da = geometry_polygon_ds
358
+ actual = cfxr.shapely_to_cf(in_da.values)
359
+ xr.testing.assert_identical(actual, expected)
360
+
361
+
362
+ @requires_shapely
363
+ def test_shapely_to_cf_for_polygons_without_multipolygons(
364
+ geometry_polygon_without_multipolygons_ds,
365
+ ):
366
+ expected, in_da = geometry_polygon_without_multipolygons_ds
367
+ actual = cfxr.shapely_to_cf(in_da)
368
+ xr.testing.assert_identical(actual, expected)
369
+
370
+
371
+ @requires_shapely
372
+ def test_shapely_to_cf_for_polygons_without_holes(
373
+ geometry_polygon_without_holes_ds,
374
+ ):
375
+ expected, in_da = geometry_polygon_without_holes_ds
376
+ actual = cfxr.shapely_to_cf(in_da)
377
+ xr.testing.assert_identical(actual, expected)
378
+
379
+
195
380
  @requires_shapely
196
381
  def test_shapely_to_cf_errors():
197
382
  from shapely.geometry import Point, Polygon
@@ -199,13 +384,8 @@ def test_shapely_to_cf_errors():
199
384
  geoms = [
200
385
  Polygon([[1, 1], [1, 3], [3, 3], [1, 1]]),
201
386
  Polygon([[1, 1, 4], [1, 3, 4], [3, 3, 3], [1, 1, 4]]),
387
+ Point(1, 2),
202
388
  ]
203
- with pytest.raises(
204
- NotImplementedError, match="Polygon geometry conversion is not implemented"
205
- ):
206
- cfxr.shapely_to_cf(geoms)
207
-
208
- geoms.append(Point(1, 2))
209
389
  with pytest.raises(ValueError, match="Mixed geometry types are not supported"):
210
390
  cfxr.shapely_to_cf(geoms)
211
391
 
@@ -247,7 +427,7 @@ def test_cf_to_shapely_for_lines_without_multilines(
247
427
  in_ds, expected = geometry_line_without_multilines_ds
248
428
  actual = cfxr.cf_to_shapely(in_ds)
249
429
  assert actual.dims == ("index",)
250
- xr.testing.assert_identical(actual, expected)
430
+ xr.testing.assert_identical(actual.drop_vars(["crd_x", "crd_y"]), expected)
251
431
 
252
432
  in_ds = in_ds.assign_coords(index=["b", "c"])
253
433
  actual = cfxr.cf_to_shapely(in_ds)
@@ -258,12 +438,51 @@ def test_cf_to_shapely_for_lines_without_multilines(
258
438
 
259
439
 
260
440
  @requires_shapely
261
- def test_cf_to_shapely_errors(geometry_ds, geometry_line_ds):
262
- in_ds, _ = geometry_ds
263
- in_ds.geometry_container.attrs["geometry_type"] = "polygon"
264
- with pytest.raises(NotImplementedError, match="Polygon geometry"):
265
- cfxr.cf_to_shapely(in_ds)
441
+ def test_cf_to_shapely_for_polygons(geometry_polygon_ds):
442
+ in_ds, expected = geometry_polygon_ds
443
+
444
+ actual = cfxr.cf_to_shapely(in_ds)
445
+ assert actual.dims == ("index",)
446
+ xr.testing.assert_identical(actual.drop_vars(["crd_x", "crd_y"]), expected)
447
+
448
+
449
+ @requires_shapely
450
+ def test_cf_to_shapely_for_polygons_without_multipolygons(
451
+ geometry_polygon_without_multipolygons_ds,
452
+ ):
453
+ in_ds, expected = geometry_polygon_without_multipolygons_ds
454
+ actual = cfxr.cf_to_shapely(in_ds)
455
+ assert actual.dims == ("index",)
456
+ xr.testing.assert_identical(actual.drop_vars(["crd_x", "crd_y"]), expected)
457
+
458
+ in_ds = in_ds.assign_coords(index=["b", "c"])
459
+ actual = cfxr.cf_to_shapely(in_ds)
460
+ assert actual.dims == ("index",)
461
+ xr.testing.assert_identical(
462
+ actual.drop_vars(["crd_x", "crd_y"]), expected.assign_coords(index=["b", "c"])
463
+ )
464
+
266
465
 
466
+ @requires_shapely
467
+ def test_cf_to_shapely_for_polygons_without_holes(
468
+ geometry_polygon_without_holes_ds,
469
+ ):
470
+ in_ds, expected = geometry_polygon_without_holes_ds
471
+ actual = cfxr.cf_to_shapely(in_ds)
472
+ assert actual.dims == ("index",)
473
+ xr.testing.assert_identical(actual.drop_vars(["crd_x", "crd_y"]), expected)
474
+
475
+ in_ds = in_ds.assign_coords(index=["b", "c"])
476
+ actual = cfxr.cf_to_shapely(in_ds)
477
+ assert actual.dims == ("index",)
478
+ xr.testing.assert_identical(
479
+ actual.drop_vars(["crd_x", "crd_y"]), expected.assign_coords(index=["b", "c"])
480
+ )
481
+
482
+
483
+ @requires_shapely
484
+ def test_cf_to_shapely_errors(geometry_ds, geometry_line_ds, geometry_polygon_ds):
485
+ in_ds, _ = geometry_ds
267
486
  in_ds.geometry_container.attrs["geometry_type"] = "punkt"
268
487
  with pytest.raises(ValueError, match="Valid CF geometry types are "):
269
488
  cfxr.cf_to_shapely(in_ds)
@@ -273,6 +492,11 @@ def test_cf_to_shapely_errors(geometry_ds, geometry_line_ds):
273
492
  with pytest.raises(ValueError, match="'node_count' must be provided"):
274
493
  cfxr.cf_to_shapely(in_ds)
275
494
 
495
+ in_ds, _ = geometry_polygon_ds
496
+ del in_ds.geometry_container.attrs["node_count"]
497
+ with pytest.raises(ValueError, match="'node_count' must be provided"):
498
+ cfxr.cf_to_shapely(in_ds)
499
+
276
500
 
277
501
  @requires_shapely
278
502
  def test_reshape_unique_geometries(geometry_ds):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
- Name: cf-xarray
3
- Version: 0.8.7
2
+ Name: cf_xarray
3
+ Version: 0.8.8
4
4
  Summary: A convenience wrapper for using CF attributes on xarray objects
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -0,0 +1,3 @@
1
+ ignore:
2
+ - "*/tests/*"
3
+ - "cf_xarray/datasets.py"
@@ -3,6 +3,11 @@
3
3
  What's New
4
4
  ----------
5
5
 
6
+ v0.8.8 (Jan 19, 2023)
7
+ =====================
8
+ - Add conversion between CF geometries and Shapely objects for polygons. By `Julia Signell`_.
9
+ - Support 32bit wide bit masks. By `Michael St Laurent`_.
10
+
6
11
  v0.8.7 (Dec 19, 2023)
7
12
  =====================
8
13
  - Add conversion between CF geometries and Shapely objects for lines. By `Julia Signell`_.
@@ -253,4 +258,5 @@ v0.1.3
253
258
  .. _`Aidan Heerdegen`: https://github.com/aidanheerdegen
254
259
  .. _`Clément Haëck`: https://github.com/Descanonge
255
260
  .. _`Julia Signell`: https://github.com/jsignell
261
+ .. _`Michael St Laurent`: https://github.com/mps010160
256
262
  .. _`flag variables`: http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#flags
@@ -1 +0,0 @@
1
- __version__ = "0.8.7"
@@ -1,2 +0,0 @@
1
- ignore:
2
- - "*/tests/*"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes