Skip to content
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ classifiers = [
"License :: OSI Approved :: BSD License",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"Topic :: Scientific/Engineering",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Visualization",
]
Expand Down
2 changes: 1 addition & 1 deletion src/nexusformat/nexus/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def is_valid_bool(dtype):
bool
True if the data type is a valid boolean type, False otherwise.
"""
return np.issubdtype(dtype, np.bool_)
return np.issubdtype(dtype, np.bool_) or is_valid_int(dtype)


def is_valid_char(dtype):
Expand Down
147 changes: 73 additions & 74 deletions src/nexusformat/nexus/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,44 @@ def is_valid_link(self, item):
self.log(f'This is a broken link to "{target}"', level='error')
return False

def check_occurrences(self, key, tag, occurrences):
"""
Checks if the number of occurrences of a group is valid.

Parameters
----------
tag : str
The XML tag of the group.
occurrences : int
The number of occurrences of the group.
"""
recommended = False
if '@minOccurs' in tag:
minOccurs = int(tag['@minOccurs'])
elif tag.get('@optional') == 'true':
minOccurs = 0
elif tag.get('@recommended') == 'true':
minOccurs = 0
recommended = True
else:
minOccurs = 1
if occurrences == 0 and minOccurs > 0:
self.log(f'This required {key} is not in the NeXus file',
level='error')
elif occurrences == 0 and recommended:
self.log(f'This recommended {key} is not in the NeXus file',
level='warning')
elif occurrences == 0:
self.log(f'This optional {key} is not in the NeXus file')
if '@maxOccurs' in tag:
try:
maxOccurs = int(tag['@maxOccurs'])
except ValueError:
maxOccurs = None
if maxOccurs is not None and occurrences > maxOccurs:
self.log(f'This {key} has more occurrences than allowed',
level='error')

def log(self, message, level='info', indent=None):
"""
Logs a message with a specified level and indentation.
Expand Down Expand Up @@ -686,6 +724,10 @@ def check_dimensions(self, field, dimensions):
"""
Checks the field dimensions against the specified dimensions.

Note that inconsistencies with the NeXus definition are not
listed as warnings, because this aspect of the NeXus standard
needs to be revised.

Parameters
----------
field :
Expand All @@ -707,7 +749,7 @@ def check_dimensions(self, field, dimensions):
self.log(f'The field has the correct rank of {rank}')
else:
self.log(f'The field has rank {field.ndim}, '
f'should be {rank}', level='warning')
f'should be {rank}')
if 'dim' in dimensions:
for i, s in dimensions['dim'].items():
if s in self.parent.symbols:
Expand All @@ -717,8 +759,7 @@ def check_dimensions(self, field, dimensions):
else:
self.log(
f'The field rank is {len(field.shape)}, '
f'but the dimension index of "{s}" = {i}',
level='warning')
f'but the dimension index of "{s}" = {i}')
else:
try:
s = int(s)
Expand All @@ -728,7 +769,7 @@ def check_dimensions(self, field, dimensions):
self.log(f'The field has the correct size of {s}')
else:
self.log(f'The field has size {field.shape}, '
f'should be {s}', level='warning')
f'should be {s}')

def check_enumeration(self, field, enumerations):
"""
Expand Down Expand Up @@ -764,10 +805,10 @@ def check_attributes(self, field, attributes=None, units=None):
if 'signal' in field.attrs:
self.log(
'Using "signal" as a field attribute is no longer valid. '
'Use the group attribute "signal"', level='error')
'Use the group attribute "signal"', level='warning')
elif 'axis' in field.attrs:
self.log('Using "axis" as a field attribute is no longer valid. '
'Use the group attribute "axes"', level='error')
'Use the group attribute "axes"', level='warning')
if 'units' in field.attrs:
if units:
self.log(
Expand Down Expand Up @@ -798,8 +839,7 @@ def check_attributes(self, field, attributes=None, units=None):
for attr in [a for a in field.attrs if a not in checked_attributes]:
self.log(f'The attribute "@{attr}" is present')

def validate(self, tag, field, parent=None, minOccurs=None, link=False,
indent=0):
def validate(self, tag, field, parent=None, link=False, indent=0):
"""
Validates a field in a NeXus group.

Expand All @@ -811,8 +851,6 @@ def validate(self, tag, field, parent=None, minOccurs=None, link=False,
The field to be validated.
parent : object, optional
The parent object. Defaults to None.
minOccurs : int, optional
The minimum number of occurrences. Defaults to None.
link : bool, optional
True if the field is required to be a link. Defaults to
False.
Expand All @@ -827,6 +865,10 @@ def validate(self, tag, field, parent=None, minOccurs=None, link=False,
else:
self.log(f'Field: {field.nxpath}', level='all')
self.indent += 1
if tag is not None and field.exists():
self.check_occurrences('field', tag, 1)
elif tag is not None:
self.check_occurrences('field', tag, 0)
if not is_valid_name(field.nxname):
self.log(f'"{field.nxname}" is an invalid name', level='error')
if link and not isinstance(field, NXlink):
Expand All @@ -835,17 +877,6 @@ def validate(self, tag, field, parent=None, minOccurs=None, link=False,
if not self.is_valid_link(field):
self.output_log()
return
if minOccurs is not None:
if minOccurs > 0:
self.log('This is a required field in the NeXus file')
else:
self.log('This is an optional field in the NeXus file')
elif tag is not None:
if '@name' in tag:
self.log(f'This field name matches "{tag["@name"]}", '
f'which is allowed in {group.nxclass}')
else:
self.log(f'This is a valid field in {group.nxclass}')
if tag is None:
if self.parent.ignoreExtraFields is True:
self.log(f'This field is not defined in {group.nxclass} '
Expand All @@ -854,6 +885,11 @@ def validate(self, tag, field, parent=None, minOccurs=None, link=False,
self.log(f'This field is not defined in {group.nxclass}',
level='warning')
else:
if '@name' in tag:
self.log(f'This field name matches "{tag["@name"]}", '
f'which is allowed in {group.nxclass}')
else:
self.log(f'This is a valid field in {group.nxclass}')
if '@deprecated' in tag:
self.log(f'This field is now deprecated. {tag["@deprecated"]}',
level='warning')
Expand Down Expand Up @@ -1056,19 +1092,10 @@ def validate_group(self, xml_dict, nxgroup, indent=0):
for key, value in xml_dict.items():
if key == 'group':
for group in value:
recommended = False
if '@minOccurs' in value[group]:
minOccurs = int(value[group]['@minOccurs'])
elif value[group].get('@optional') == 'true':
minOccurs = 0
elif value[group].get('@recommended') == 'true':
minOccurs = 0
recommended = True
else:
minOccurs = 1
if '@type' in value[group]:
tag = value[group]
if '@type' in tag:
name = group
group = value[group]['@type']
group = tag['@type']
self.log(f'Group: {name}: {group}', level='all',
indent=self.indent)
nxgroups = [g for g in nxgroup.component(group)
Expand All @@ -1079,67 +1106,39 @@ def validate_group(self, xml_dict, nxgroup, indent=0):
indent=self.indent)
nxgroups = nxgroup.component(group)
self.indent += 1
if len(nxgroups) < minOccurs:
self.log(
f'{len(nxgroups)} {group} group(s) '
f'are in the NeXus file. At least {minOccurs} '
'are required', level='error')
elif len(nxgroups) == 0 and recommended:
self.log(
'This recommended group is not in the NeXus file',
level='warning')
elif len(nxgroups) == 0:
self.log(
'This optional group is not in the NeXus file')
self.check_occurrences('group', tag, len(nxgroups))
for i, nxsubgroup in enumerate(nxgroups):
if name:
if i != 0:
self.log(f'Group: {name}: {group}',
level='all', indent=self.indent)
self.validate_group(
value[name], nxsubgroup, indent=self.indent)
self.validate_group(value[name], nxsubgroup,
indent=self.indent)
else:
if i != 0:
self.log(f'Group: {group}', level='all',
indent=self.indent)
self.validate_group(
value[group], nxsubgroup, indent=self.indent)
self.validate_group(tag, nxsubgroup,
indent=self.indent)
self.indent -= 1
self.output_log()
self.output_log()
elif key == 'field' or key == 'link':
for field in value:
recommended = False
if '@minOccurs' in value[field]:
minOccurs = int(value[field]['@minOccurs'])
elif value[field].get('@optional') == 'true':
minOccurs = 0
elif value[field].get('@recommended') == 'true':
minOccurs = 0
recommended = True
else:
minOccurs = 1
tag = value[field]
if field in nxgroup.entries:
group_validator.symbols.update(self.symbols)
field_validator.validate(
value[field], nxgroup[field], link=(key=='link'),
parent=self, minOccurs=minOccurs,
indent=self.indent)
field_validator.validate(tag, nxgroup[field],
link=(key=='link'),
parent=self,
indent=self.indent)
else:
field_path = nxgroup.nxpath + '/' + field
self.log(f'{key.capitalize()}: {field_path}',
level='all')
self.indent += 1
if minOccurs > 0:
self.log(f'This required {key} is not '
'in the NeXus file', level='error')
elif recommended:
self.log(f'This recommended {key} is not '
'in the NeXus file', level='warning')
else:
self.log(f'This optional {key}) is not '
'in the NeXus file')
self.check_occurrences('field', tag, 0)
self.indent -= 1
self.output_log()
self.output_log()
group_validator.check_symbols(indent=self.indent)
self.output_log()

Expand Down