ewoksdata 2.3.0rc2__tar.gz → 2.3.0rc3__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 (47) hide show
  1. {ewoksdata-2.3.0rc2/src/ewoksdata.egg-info → ewoksdata-2.3.0rc3}/PKG-INFO +1 -1
  2. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/pyproject.toml +1 -1
  3. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/mirror.py +204 -63
  4. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/test_mirror_hdf5.py +280 -107
  5. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3/src/ewoksdata.egg-info}/PKG-INFO +1 -1
  6. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/LICENSE.md +0 -0
  7. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/README.md +0 -0
  8. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/setup.cfg +0 -0
  9. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/__init__.py +0 -0
  10. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/__init__.py +0 -0
  11. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/bliss.py +0 -0
  12. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/blissdata/__init__.py +0 -0
  13. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/blissdata/_lima.py +0 -0
  14. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/blissdata/blissdatav0.py +0 -0
  15. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/blissdata/blissdatav1.py +0 -0
  16. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/blissdata/blissdatav2.py +0 -0
  17. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/contextiterator.py +0 -0
  18. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/__init__.py +0 -0
  19. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/config.py +0 -0
  20. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/dataset_writer/__init__.py +0 -0
  21. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/dataset_writer/_base.py +0 -0
  22. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential.py +0 -0
  23. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential_stack.py +0 -0
  24. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/dataset_writer/_sequential.py +0 -0
  25. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/dataset_writer/_sequential_stack.py +0 -0
  26. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/hdf5/types.py +0 -0
  27. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/nexus.py +0 -0
  28. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/url.py +0 -0
  29. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/data/utils.py +0 -0
  30. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/__init__.py +0 -0
  31. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/integration/__init__.py +0 -0
  32. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/integration/conftest.py +0 -0
  33. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/integration/test_blissdata.py +0 -0
  34. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/__init__.py +0 -0
  35. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/conftest.py +0 -0
  36. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/data/__init__.py +0 -0
  37. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/data/bliss_scans.py +0 -0
  38. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/test_contextiterator.py +0 -0
  39. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/test_data_bliss.py +0 -0
  40. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/test_data_hdf5.py +0 -0
  41. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/test_data_nexus.py +0 -0
  42. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/test_dataset_writer.py +0 -0
  43. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata/tests/unit/test_find_lima_scans.py +0 -0
  44. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata.egg-info/SOURCES.txt +0 -0
  45. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata.egg-info/dependency_links.txt +0 -0
  46. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata.egg-info/requires.txt +0 -0
  47. {ewoksdata-2.3.0rc2 → ewoksdata-2.3.0rc3}/src/ewoksdata.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ewoksdata
3
- Version: 2.3.0rc2
3
+ Version: 2.3.0rc3
4
4
  Summary: Ewoks tasks for data access
5
5
  Author-email: ESRF <dau-pydev@esrf.fr>
6
6
  License: # MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ewoksdata"
7
- version = "2.3.0rc2"
7
+ version = "2.3.0rc3"
8
8
  authors = [{ name = "ESRF", email = "dau-pydev@esrf.fr" }]
9
9
  description = "Ewoks tasks for data access"
10
10
  readme = { file = "README.md", content-type = "text/markdown" }
@@ -1,11 +1,14 @@
1
1
  import logging
2
2
  import os
3
+ from typing import Any
3
4
  from typing import NamedTuple
5
+ from typing import Optional
4
6
  from typing import Sequence
5
7
  from typing import Set
6
8
  from typing import Union
7
9
 
8
10
  import h5py
11
+ import numpy
9
12
  from silx.io.url import DataUrl
10
13
 
11
14
  # For type annotations only. Code works for any h5py-like objects.
@@ -44,7 +47,8 @@ def mirror_hdf5_content(
44
47
 
45
48
  HDF5 links are soft links, external links and virtual datasets.
46
49
 
47
- An HDF5 link with target(s) within :code:`original` is always kept as a link.
50
+ An HDF5 link with target(s) within :code:`original` is always kept as a link,
51
+ re-targeted to the mirror of those target(s).
48
52
 
49
53
  An HDF5 link with target(s) outside :code:`original` is called out-of-scope.
50
54
  An out-of-scope link is kept as a link to wherever the data already is
@@ -78,10 +82,11 @@ def mirror_hdf5_content(
78
82
  expand_paths = _normalize_relative_paths(expand)
79
83
  recursive_paths = _normalize_relative_paths(recursive)
80
84
 
81
- home = DataUrl(
85
+ home_original = DataUrl(
82
86
  file_path=os.path.abspath(_get_hdf5_filename(original.file)),
83
87
  data_path=original.name,
84
88
  )
89
+ home_destination = _normalize_hdf5_item_name(parent.name, name)
85
90
 
86
91
  _mirror(
87
92
  parent,
@@ -94,7 +99,8 @@ def mirror_hdf5_content(
94
99
  expand=expand_paths,
95
100
  recursive=recursive_paths,
96
101
  copy=copy,
97
- home=home,
102
+ home_original=home_original,
103
+ home_destination=home_destination,
98
104
  overwrite=overwrite,
99
105
  allow_cross_file=allow_cross_file,
100
106
  )
@@ -103,6 +109,7 @@ def mirror_hdf5_content(
103
109
  class _ReachedBy(NamedTuple):
104
110
  link: Union[None, h5py.HardLink, h5py.SoftLink, h5py.ExternalLink]
105
111
  in_scope: bool
112
+ soft_link_target: Optional[str] = None
106
113
 
107
114
  @property
108
115
  def is_soft_link_in_scope(self) -> bool:
@@ -115,17 +122,21 @@ class _ReachedBy(NamedTuple):
115
122
  )
116
123
 
117
124
  @classmethod
118
- def from_h5py(cls, parent: GroupType, name: str, home: DataUrl) -> "_ReachedBy":
125
+ def from_h5py(
126
+ cls, parent: GroupType, name: str, home_original: DataUrl
127
+ ) -> "_ReachedBy":
119
128
  link = _get_hdf5_link(parent, name)
129
+ soft_link_target = None
120
130
  if isinstance(link, h5py.SoftLink):
121
- in_scope = _soft_link_in_scope(parent, link, home)
131
+ soft_link_target = _absolute_soft_link_target(parent, link)
132
+ in_scope = _soft_link_in_scope(parent, soft_link_target, home_original)
122
133
  elif isinstance(link, h5py.ExternalLink):
123
134
  in_scope = False
124
135
  elif isinstance(link, h5py.HardLink):
125
136
  in_scope = True
126
137
  else:
127
138
  raise NotImplementedError(f"Unknown HDF5 link type: {type(link)}")
128
- return _ReachedBy(link, in_scope)
139
+ return _ReachedBy(link, in_scope, soft_link_target)
129
140
 
130
141
 
131
142
  def _mirror(
@@ -139,7 +150,8 @@ def _mirror(
139
150
  expand: Set[str],
140
151
  recursive: Set[str],
141
152
  copy: bool,
142
- home: DataUrl,
153
+ home_original: DataUrl,
154
+ home_destination: str,
143
155
  overwrite: bool,
144
156
  allow_cross_file: bool,
145
157
  ) -> None:
@@ -164,12 +176,16 @@ def _mirror(
164
176
  :param copy: copy instead of link by default at this level (already
165
177
  accounts for any :code:`invert` entry applied by an
166
178
  ancestor).
167
- :param home: file and HDF5 path of the root original; used to never
168
- physically copy data reached through a link into another
169
- file (unless :code:`allow_cross_file`), and to tell
170
- whether a soft link or virtual dataset source refers to a
171
- path guaranteed to exist in :code:`parent` (within
172
- :code:`home`'s tree) or not.
179
+ :param home_original: file and HDF5 path of the root original; used to
180
+ never physically copy data reached through a link
181
+ into another file (unless :code:`allow_cross_file`),
182
+ and to tell whether a soft link or virtual dataset
183
+ source refers to a path guaranteed to exist in
184
+ :code:`parent` (within :code:`home_original`'s tree)
185
+ or not.
186
+ :param home_destination: absolute HDF5 path of the mirror of
187
+ :code:`home_original`; used to re-target a link
188
+ to a path within :code:`home_original`'s tree.
173
189
  :param overwrite: delete a conflicting existing name instead of warning
174
190
  and leaving it untouched.
175
191
  :param allow_cross_file: allow crossing into a file other than the
@@ -219,7 +235,7 @@ def _mirror(
219
235
  # than the one being mirrored: never allowed unless `allow_cross_file`,
220
236
  # regardless of what `expand`/`recursive` ask for (see `traverse` below).
221
237
  crosses_file = (
222
- os.path.abspath(_get_hdf5_filename(original.file)) != home.file_path()
238
+ os.path.abspath(_get_hdf5_filename(original.file)) != home_original.file_path()
223
239
  )
224
240
 
225
241
  # Open `original` as a group instead of mirroring it as a single link or
@@ -243,7 +259,7 @@ def _mirror(
243
259
  destination = parent.create_group(name)
244
260
  _update_attrs(destination, original, overwrite)
245
261
  for child_name in original:
246
- child_reached_by = _ReachedBy.from_h5py(original, child_name, home)
262
+ child_reached_by = _ReachedBy.from_h5py(original, child_name, home_original)
247
263
  try:
248
264
  original_child = _resolve_hdf5_group_child(original, child_name)
249
265
  except KeyError:
@@ -262,7 +278,8 @@ def _mirror(
262
278
  expand=expand,
263
279
  recursive=recursive,
264
280
  copy=do_copy,
265
- home=home,
281
+ home_original=home_original,
282
+ home_destination=home_destination,
266
283
  overwrite=overwrite,
267
284
  allow_cross_file=allow_cross_file,
268
285
  )
@@ -274,23 +291,35 @@ def _mirror(
274
291
  # `name` is ensured to not exist in `parent`
275
292
 
276
293
  if not do_copy:
294
+ if reached_by.is_soft_link_in_scope and _create_mirrored_soft_link(
295
+ parent, name, reached_by, home_original, home_destination, exclude
296
+ ):
297
+ # Preserve the link faithfully instead of linking to what it
298
+ # points to in the file being mirrored.
299
+ return
277
300
  _create_hdf5_link(parent, name, original, overwrite=overwrite)
278
301
  return
279
302
 
280
303
  # The content is being copied. First try preserving links faithfully.
281
304
 
282
- if reached_by.is_soft_link_in_scope:
283
- # Preserve the link faithfully instead of copying the data it
284
- # points to. Same file, so there is no path to re-adjust.
285
- parent[name] = reached_by.link
305
+ if reached_by.is_soft_link_in_scope and _create_mirrored_soft_link(
306
+ parent, name, reached_by, home_original, home_destination, exclude
307
+ ):
308
+ # Preserve the link faithfully instead of copying the data it points to.
286
309
  return
287
310
 
288
- is_vds = not is_group and original.is_virtual
289
- if is_vds and _virtual_dataset_in_scope(original, home):
311
+ # Virtual datasets and copies are handled with the h5py dataset
312
+ # underneath, when there is one.
313
+ h5py_original = None if is_group else _as_h5py_item(original)
314
+
315
+ is_vds = h5py_original is not None and h5py_original.is_virtual
316
+ if is_vds and _virtual_dataset_in_scope(h5py_original, home_original):
290
317
  # Every source of this VDS resolves within the tree being mirrored,
291
318
  # which is guaranteed to also exist in `parent`'s file: preserve it
292
319
  # faithfully as a VDS.
293
- if _copy_virtual_dataset(parent, name, original, home.data_path()):
320
+ if _copy_virtual_dataset(
321
+ parent, name, h5py_original, home_original, home_destination
322
+ ):
294
323
  return
295
324
 
296
325
  if allow_cross_file:
@@ -298,8 +327,8 @@ def _mirror(
298
327
  # Materialize instead of preserving the VDS: the whole point of
299
328
  # allow_cross_file is to no longer depend on the file being
300
329
  # mirrored, which is the only place this source resolves in.
301
- dataset = parent.create_dataset(name, data=original[()])
302
- dataset.attrs.update(original.attrs)
330
+ dataset = parent.create_dataset(name, data=h5py_original[()])
331
+ dataset.attrs.update(h5py_original.attrs)
303
332
  return
304
333
  else:
305
334
  # When possible, preserve links faithfully.
@@ -314,13 +343,15 @@ def _mirror(
314
343
  return
315
344
  if is_vds:
316
345
  # Re-create the VDS (no always possible) to recomputed relative file path.
317
- if _copy_virtual_dataset(parent, name, original, home.data_path()):
346
+ if _copy_virtual_dataset(
347
+ parent, name, h5py_original, home_original, home_destination
348
+ ):
318
349
  return
319
350
 
320
351
  # Copy the actual content
321
352
 
322
353
  if not is_group:
323
- parent.copy(original, name)
354
+ _copy_hdf5_dataset(parent, name, original, h5py_original)
324
355
  return
325
356
 
326
357
  # Recreate the group instead of `h5py.Group.copy` to handle links.
@@ -337,14 +368,19 @@ def _mirror(
337
368
  # {""} forces the whole subtree to be traversed (see `in_recursive`).
338
369
  recursive={""},
339
370
  copy=True,
340
- home=home,
371
+ home_original=home_original,
372
+ home_destination=home_destination,
341
373
  overwrite=overwrite,
342
374
  allow_cross_file=allow_cross_file,
343
375
  )
344
376
 
345
377
 
346
378
  def _copy_virtual_dataset(
347
- parent: GroupType, name: str, original: DatasetType, home_path: str
379
+ parent: GroupType,
380
+ name: str,
381
+ original: h5py.Dataset,
382
+ home_original: DataUrl,
383
+ home_destination: str,
348
384
  ) -> bool:
349
385
  """Try to faithfully copy virtual dataset :code:`original` under :code:`name`
350
386
  in :code:`parent`, by reconstructing its virtual sources with paths
@@ -352,8 +388,10 @@ def _copy_virtual_dataset(
352
388
  mapping verbatim (which does not adjust source paths when :code:`parent`
353
389
  is not in the same directory as :code:`original`).
354
390
 
355
- :param home_path: absolute HDF5 path of the root original being mirrored;
356
- see :code:`_mirror`.
391
+ :param home_original: file and HDF5 path of the root original being
392
+ mirrored; see :code:`_mirror`.
393
+ :param home_destination: absolute HDF5 path of the mirror of :code:`home_original`;
394
+ see :code:`_mirror`.
357
395
  :returns: :code:`False` (nothing created) when a source mapping is not a
358
396
  single, whole-source hyperslab, the only shape this can
359
397
  faithfully reconstruct; the caller should fall back to a plain
@@ -369,6 +407,7 @@ def _copy_virtual_dataset(
369
407
  return False
370
408
  mappings.append(mapping)
371
409
 
410
+ home_original_path = home_original.data_path()
372
411
  layout = h5py.VirtualLayout(shape=original.shape, dtype=original.dtype)
373
412
  original_filename = _get_hdf5_filename(original.file)
374
413
  parent_filename = _get_hdf5_filename(parent.file)
@@ -376,11 +415,16 @@ def _copy_virtual_dataset(
376
415
  start, stop = mapping.vspace.get_select_bounds()
377
416
  region = tuple(slice(a, b + 1) for a, b in zip(start, stop))
378
417
  region_shape = tuple(b - a + 1 for a, b in zip(start, stop))
418
+ vsource_dset_name = mapping.dset_name
379
419
  if mapping.file_name == ".":
380
- if _in_scope(mapping.dset_name, home_path):
381
- # Keep it as-is, since `mapping.dset_name` is guaranteed
382
- # to also exist in `parent`'s file.
420
+ if _in_scope(mapping.dset_name, home_original_path):
421
+ # Point to the mirror of the source, which is guaranteed to
422
+ # exist in `parent`'s file.
383
423
  vsource_filename = "."
424
+ vsource_dset_name = _normalize_hdf5_item_name(
425
+ home_destination,
426
+ _relative_to_home_original(mapping.dset_name, home_original),
427
+ )
384
428
  else:
385
429
  # "." pointing outside the tree being mirrored: that path is
386
430
  # only guaranteed to exist in `original`'s own file, so point
@@ -395,7 +439,7 @@ def _copy_virtual_dataset(
395
439
  parent_filename, original_filename, mapping.file_name
396
440
  )
397
441
  source = h5py.VirtualSource(
398
- vsource_filename, mapping.dset_name, shape=region_shape
442
+ vsource_filename, vsource_dset_name, shape=region_shape
399
443
  )
400
444
  layout[region] = source
401
445
  parent.create_virtual_dataset(name, layout, fillvalue=original.fillvalue)
@@ -418,6 +462,21 @@ def _relative_virtual_source_filename(
418
462
  return os.path.relpath(abs_vsource_filename, abs_parent_dir)
419
463
 
420
464
 
465
+ def _copy_hdf5_dataset(
466
+ parent: GroupType,
467
+ name: str,
468
+ original: DatasetType,
469
+ h5py_original: Optional[h5py.Dataset],
470
+ ) -> None:
471
+ """Copy dataset :code:`original` under :code:`name` in :code:`parent`."""
472
+ if h5py_original is not None:
473
+ parent.copy(h5py_original, name)
474
+ else:
475
+ # `h5py.Group.copy` only accepts h5py objects.
476
+ dataset = parent.create_dataset(name, data=original[()])
477
+ dataset.attrs.update(original.attrs)
478
+
479
+
421
480
  def _resolve_hdf5_group_child(
422
481
  parent: GroupType, name: str
423
482
  ) -> Union[DatasetType, GroupType]:
@@ -427,51 +486,115 @@ def _resolve_hdf5_group_child(
427
486
  after resolving has the correct `:code:.file` but not the correct `:code:.name`.
428
487
  """
429
488
  link = _get_hdf5_link(parent, name)
430
- if not isinstance(link, h5py.SoftLink):
431
- return parent[name]
432
489
 
433
- # Resolve the target path of the soft link.
434
- target_path = link.path
435
- if not target_path.startswith("/"):
436
- target_path = _normalize_hdf5_item_name(parent.name, target_path)
490
+ if isinstance(link, h5py.SoftLink):
491
+ # Resolve the target of the soft link instead of resolving the soft
492
+ # link, as the target may be an external link itself.
493
+ target_path = _absolute_soft_link_target(parent, link)
494
+ return _resolve_hdf5_item(parent.file, target_path)
437
495
 
438
- # Resolve the target of the soft link instead of resolving the soft link.
439
- return parent.file[target_path]
496
+ if isinstance(link, h5py.ExternalLink):
497
+ # An h5py-like object behind an external link has no way of telling
498
+ # which file it belongs to: use the h5py object underneath, which does.
499
+ h5py_parent = _as_h5py_item(parent)
500
+ if h5py_parent is not None:
501
+ return h5py_parent[name]
440
502
 
503
+ return parent[name]
441
504
 
442
- def _soft_link_in_scope(
505
+
506
+ def _resolve_hdf5_item(root: GroupType, path: str) -> Union[DatasetType, GroupType]:
507
+ """Resolve the absolute HDF5 path :code:`path` in :code:`root`, one link at
508
+ a time.
509
+ """
510
+ item = root
511
+ for name in path.split("/"):
512
+ if name:
513
+ item = _resolve_hdf5_group_child(item, name)
514
+ return item
515
+
516
+
517
+ def _create_mirrored_soft_link(
443
518
  parent: GroupType,
444
- link: Union[h5py.HardLink, h5py.SoftLink, h5py.ExternalLink],
445
- home: DataUrl,
519
+ name: str,
520
+ reached_by: "_ReachedBy",
521
+ home_original: DataUrl,
522
+ home_destination: str,
523
+ exclude: Set[str],
446
524
  ) -> bool:
447
- """Does :code:`link` refer to a path within :code:`home`'s path?"""
448
- if not isinstance(link, h5py.SoftLink):
525
+ """Create a soft link under :code:`name` in :code:`parent`, pointing to the
526
+ mirror of the target of the in-scope soft link :code:`reached_by`.
527
+
528
+ :returns: :code:`False` when the target is not mirrored, in which case
529
+ nothing is created and the caller has to fall back to
530
+ referring to the target itself.
531
+ """
532
+ relative_target = _relative_to_home_original(
533
+ reached_by.soft_link_target, home_original
534
+ )
535
+ if any(
536
+ relative_target == path or relative_target.startswith(path + "/")
537
+ for path in exclude
538
+ ):
449
539
  return False
450
- if os.path.abspath(_get_hdf5_filename(parent.file)) != home.file_path():
540
+ filename = _get_hdf5_filename(parent.file)
541
+ link = _hdf5_link_object(
542
+ parent_filename=filename,
543
+ parent_name=parent.name,
544
+ link_name=name,
545
+ target_filename=filename,
546
+ target_name=_normalize_hdf5_item_name(home_destination, relative_target),
547
+ )
548
+ if link is not None:
549
+ parent[name] = link
550
+ return True
551
+
552
+
553
+ def _relative_to_home_original(absolute_path: str, home_original: DataUrl) -> str:
554
+ """HDF5 path of :code:`absolute_path` relative to :code:`home_original`'s path,
555
+ empty for :code:`home_original` itself.
556
+ """
557
+ relative_path = os.path.relpath(absolute_path, home_original.data_path())
558
+ if relative_path == ".":
559
+ return ""
560
+ return relative_path
561
+
562
+
563
+ def _absolute_soft_link_target(parent: GroupType, link: h5py.SoftLink) -> str:
564
+ """Absolute HDF5 path of the target of soft link :code:`link` in :code:`parent`."""
565
+ if link.path.startswith("/"):
566
+ return _normalize_hdf5_item_name(link.path)
567
+ return _normalize_hdf5_item_name(parent.name, link.path)
568
+
569
+
570
+ def _soft_link_in_scope(
571
+ parent: GroupType, target_path: str, home_original: DataUrl
572
+ ) -> bool:
573
+ """Does the soft link target :code:`target_path` refer to a path within
574
+ :code:`home_original`'s path?
575
+ """
576
+ if os.path.abspath(_get_hdf5_filename(parent.file)) != home_original.file_path():
451
577
  return False
452
- target_path = link.path
453
- if not target_path.startswith("/"):
454
- target_path = _normalize_hdf5_item_name(parent.name, target_path)
455
- return _in_scope(target_path, home.data_path())
578
+ return _in_scope(target_path, home_original.data_path())
456
579
 
457
580
 
458
- def _virtual_dataset_in_scope(original: DatasetType, home: DataUrl) -> bool:
581
+ def _virtual_dataset_in_scope(original: h5py.Dataset, home_original: DataUrl) -> bool:
459
582
  """Does every virtual source of :code:`original` refer to a path
460
- within :code:`home`'s path?
583
+ within :code:`home_original`'s path?
461
584
  """
462
- home_path = home.data_path()
585
+ home_original_path = home_original.data_path()
463
586
  return all(
464
- mapping.file_name == "." and _in_scope(mapping.dset_name, home_path)
587
+ mapping.file_name == "." and _in_scope(mapping.dset_name, home_original_path)
465
588
  for mapping in original.virtual_sources()
466
589
  )
467
590
 
468
591
 
469
- def _in_scope(absolute_path: str, home_path: str) -> bool:
592
+ def _in_scope(absolute_path: str, home_original_path: str) -> bool:
470
593
  """Is :code:`absolute_path` (an absolute HDF5 path) equal to
471
- :code:`home_path` or a descendant of it?
594
+ :code:`home_original_path` or a descendant of it?
472
595
  """
473
- prefix = home_path.rstrip("/") + "/"
474
- return absolute_path == home_path or absolute_path.startswith(prefix)
596
+ prefix = home_original_path.rstrip("/") + "/"
597
+ return absolute_path == home_original_path or absolute_path.startswith(prefix)
475
598
 
476
599
 
477
600
  def _create_hdf5_link(
@@ -596,12 +719,16 @@ def _item_conflict(parent: GroupType, name: str, overwrite: bool) -> bool:
596
719
 
597
720
 
598
721
  def _attr_conflict(
599
- destination: Union[DatasetType, GroupType], key: str, overwrite: bool
722
+ destination: Union[DatasetType, GroupType], key: str, value: Any, overwrite: bool
600
723
  ) -> bool:
601
724
  """Returns :code:`True` when these is a conflict and it cannot be resolved."""
602
725
  if key not in destination.attrs:
603
726
  return False
604
727
 
728
+ if numpy.array_equal(destination.attrs[key], value):
729
+ # Nothing to overwrite
730
+ return True
731
+
605
732
  if not overwrite:
606
733
  logger.warning(
607
734
  "attribute %s already exists on %s: not overwritten",
@@ -620,7 +747,7 @@ def _update_attrs(
620
747
  ) -> None:
621
748
  """Copy :code:`original`'s attributes onto :code:`destination`."""
622
749
  for key, value in original.attrs.items():
623
- if key in destination.attrs and _attr_conflict(destination, key, overwrite):
750
+ if _attr_conflict(destination, key, value, overwrite):
624
751
  continue
625
752
  destination.attrs[key] = value
626
753
 
@@ -654,6 +781,20 @@ def _get_hdf5_link(
654
781
  return h5py.HardLink()
655
782
 
656
783
 
784
+ def _as_h5py_item(
785
+ item: Union[DatasetType, GroupType],
786
+ ) -> Optional[Union[h5py.Dataset, h5py.Group]]:
787
+ """The h5py object underlying :code:`item`, :code:`None` when there is none."""
788
+ # Duck-typed so it works for any h5py-like objects.
789
+ if isinstance(item, (h5py.Dataset, h5py.Group)):
790
+ return item
791
+ try:
792
+ # to be fixed in blissdata
793
+ return item._retry_handler.file_obj[item.name]
794
+ except (AttributeError, KeyError):
795
+ return None
796
+
797
+
657
798
  def _is_group(item: Union[DatasetType, GroupType]) -> bool:
658
799
  # Duck-typed so it works for any h5py-like objects.
659
800
  return hasattr(item, "keys")