Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not allow self-ref in depends_on #255

Merged
merged 1 commit into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/scippnexus/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,14 @@ class DependsOn:
value: str

def absolute_path(self) -> str | None:
if self.value == '.':
if self.is_terminal:
return None
return posixpath.normpath(posixpath.join(self.parent, self.value))

@property
def is_terminal(self) -> bool:
return self.value == '.'


def _is_time(obj):
if (unit := obj.unit) is None:
Expand Down
9 changes: 2 additions & 7 deletions src/scippnexus/nxtransformations.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,18 +310,13 @@ def parse_depends_on_chain(
file = parent.underlying.file
visited = [depends_on.absolute_path()]
try:
while depends_on.value != '.':
while not depends_on.is_terminal:
transform, base = _locate_depends_on_target(
file, depends_on, parent.definitions
)
depends_on = DependsOn(parent=base, value=transform.attrs['depends_on'])
chain.transformations[transform.name] = transform[()]
if depends_on.absolute_path() == visited[-1]:
depends_on.value = '.'
# Transform.from_object does not see the full chain, so it cannot
# detect this case.
chain.transformations[transform.name].depends_on = depends_on
elif depends_on.absolute_path() in visited:
if depends_on.absolute_path() in visited:
raise ValueError(
'Circular depends_on chain detected: '
f'{[*visited, depends_on.absolute_path()]}'
Expand Down
52 changes: 6 additions & 46 deletions tests/nxtransformations_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,7 @@ def test_compute_transformation_warns_if_transformation_missing_vector_attr(
root[()]


def test_compute_positions_terminates_with_depends_on_to_self_absolute(h5root):
def test_compute_positions_raises_for_depends_on_self_absolute(h5root):
instrument = snx.create_class(h5root, 'instrument', snx.NXinstrument)
detector = create_detector(instrument)
snx.create_field(detector, 'x_pixel_offset', sc.linspace('xx', -1, 1, 2, unit='m'))
Expand All @@ -846,7 +846,6 @@ def test_compute_positions_terminates_with_depends_on_to_self_absolute(h5root):
value = sc.scalar(6.5, unit='mm')
offset = sc.spatial.translation(value=[1, 2, 3], unit='mm')
vector = sc.vector(value=[0, 0, 1])
t = value * vector
value1 = snx.create_field(transformations, 't1', value)
value1.attrs['depends_on'] = 't2'
value1.attrs['transformation_type'] = 'translation'
Expand All @@ -858,31 +857,12 @@ def test_compute_positions_terminates_with_depends_on_to_self_absolute(h5root):
value2.attrs['transformation_type'] = 'translation'
value2.attrs['vector'] = vector.value

t1 = sc.spatial.translations(dims=t.dims, values=t.values, unit=t.unit) * offset
t2 = sc.spatial.translations(dims=t.dims, values=t.values, unit=t.unit).to(
unit='cm'
)
root = make_group(h5root)
loaded = root[()]
result = snx.compute_positions(loaded)
origin = sc.vector([0, 0, 0], unit='m')
assert_identical(
result['instrument']['detector_0']['position'],
t2.to(unit='m') * t1.to(unit='m') * origin,
)
assert_identical(
result['instrument']['detector_0']['data'].coords['position'],
t2.to(unit='m')
* t1.to(unit='m')
* sc.vectors(
dims=['xx', 'yy'],
values=[[[-1, -1, 0], [-1, 1, 0]], [[1, -1, 0], [1, 1, 0]]],
unit='m',
),
)
with pytest.raises(ValueError, match="Circular depends_on"):
_ = root[()]


def test_compute_positions_terminates_with_depends_on_to_self_relative(h5root):
def test_compute_positions_raises_for_depends_on_self_relative(h5root):
instrument = snx.create_class(h5root, 'instrument', snx.NXinstrument)
detector = create_detector(instrument)
snx.create_field(detector, 'x_pixel_offset', sc.linspace('xx', -1, 1, 2, unit='m'))
Expand All @@ -897,7 +877,6 @@ def test_compute_positions_terminates_with_depends_on_to_self_relative(h5root):
value = sc.scalar(6.5, unit='mm')
offset = sc.spatial.translation(value=[1, 2, 3], unit='mm')
vector = sc.vector(value=[0, 0, 1])
t = value * vector
value1 = snx.create_field(transformations, 't1', value)
value1.attrs['depends_on'] = 't2'
value1.attrs['transformation_type'] = 'translation'
Expand All @@ -909,28 +888,9 @@ def test_compute_positions_terminates_with_depends_on_to_self_relative(h5root):
value2.attrs['transformation_type'] = 'translation'
value2.attrs['vector'] = vector.value

t1 = sc.spatial.translations(dims=t.dims, values=t.values, unit=t.unit) * offset
t2 = sc.spatial.translations(dims=t.dims, values=t.values, unit=t.unit).to(
unit='cm'
)
root = make_group(h5root)
loaded = root[()]
result = snx.compute_positions(loaded)
origin = sc.vector([0, 0, 0], unit='m')
assert_identical(
result['instrument']['detector_0']['position'],
t2.to(unit='m') * t1.to(unit='m') * origin,
)
assert_identical(
result['instrument']['detector_0']['data'].coords['position'],
t2.to(unit='m')
* t1.to(unit='m')
* sc.vectors(
dims=['xx', 'yy'],
values=[[[-1, -1, 0], [-1, 1, 0]], [[1, -1, 0], [1, 1, 0]]],
unit='m',
),
)
with pytest.raises(ValueError, match="Circular depends_on"):
_ = root[()]


def test_compute_positions_raises_for_depends_on_cycle(h5root):
Expand Down