From 2478564d8f37addf0e326c2058d651e1461af6fb Mon Sep 17 00:00:00 2001 From: Ray Osborn Date: Sat, 13 Dec 2025 17:05:49 -0600 Subject: [PATCH 1/5] Consolidate occurrence checks in the`check_occurrences` function --- src/nexusformat/nexus/validate.py | 132 +++++++++++++++--------------- 1 file changed, 64 insertions(+), 68 deletions(-) diff --git a/src/nexusformat/nexus/validate.py b/src/nexusformat/nexus/validate.py index 73a2297..22b013c 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. @@ -798,8 +836,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 +848,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 +862,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 +874,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 +882,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 +1089,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 +1103,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() From 5006720846ecfd463bd424ac30ab6dae163f116d Mon Sep 17 00:00:00 2001 From: Ray Osborn Date: Sat, 13 Dec 2025 17:07:46 -0600 Subject: [PATCH 2/5] Demote dimension logs to informational. The specification of dimensions in the NXDL files contains some serious anomalies, which generate unnecessary warnings. They may now be viewed in the informational messages but not listed as warnings. --- src/nexusformat/nexus/validate.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/nexusformat/nexus/validate.py b/src/nexusformat/nexus/validate.py index 22b013c..ab7c34c 100644 --- a/src/nexusformat/nexus/validate.py +++ b/src/nexusformat/nexus/validate.py @@ -724,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 : @@ -745,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: @@ -755,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) @@ -766,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): """ From 48c40f7306ff143510f816ec6da466a37b96691f Mon Sep 17 00:00:00 2001 From: Ray Osborn Date: Sat, 13 Dec 2025 17:09:04 -0600 Subject: [PATCH 3/5] Demote NXfield `signal` and `axis` errors to warnings. These are still strongly deprecated. --- src/nexusformat/nexus/validate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nexusformat/nexus/validate.py b/src/nexusformat/nexus/validate.py index ab7c34c..a1451b7 100644 --- a/src/nexusformat/nexus/validate.py +++ b/src/nexusformat/nexus/validate.py @@ -805,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( From cf983e2cbff625e616ae3c0b98b2a1d205b83107 Mon Sep 17 00:00:00 2001 From: Ray Osborn Date: Sat, 13 Dec 2025 17:09:45 -0600 Subject: [PATCH 4/5] Allow integers to be used in NX_BOOLEAN fields. --- src/nexusformat/nexus/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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): From 8992cce29213fb5c31a8c73673372d62693b3f59 Mon Sep 17 00:00:00 2001 From: Ray Osborn Date: Sat, 13 Dec 2025 17:11:09 -0600 Subject: [PATCH 5/5] Add support for Pythone 3.14 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", ]