diff --git a/pyproject.toml b/pyproject.toml index 1fff56c..6dcf21f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", ] diff --git a/src/nexusformat/nexus/utils.py b/src/nexusformat/nexus/utils.py index 6df9e89..abc2f04 100644 --- a/src/nexusformat/nexus/utils.py +++ b/src/nexusformat/nexus/utils.py @@ -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): diff --git a/src/nexusformat/nexus/validate.py b/src/nexusformat/nexus/validate.py index 73a2297..a1451b7 100644 --- a/src/nexusformat/nexus/validate.py +++ b/src/nexusformat/nexus/validate.py @@ -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. @@ -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 : @@ -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: @@ -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) @@ -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): """ @@ -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( @@ -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. @@ -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. @@ -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): @@ -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} ' @@ -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') @@ -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) @@ -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()