From ee2867064c26a5bb869778fd204230156f23ae69 Mon Sep 17 00:00:00 2001 From: Brett Date: Wed, 27 Nov 2024 13:33:06 -0500 Subject: [PATCH] apply hotfix for schema info collection combiner handling --- asdf/_node_info.py | 97 +++++++++++++++++++++++++++++++++------- asdf/_tests/test_info.py | 37 ++++++++++----- asdf/exceptions.py | 6 +++ 3 files changed, 113 insertions(+), 27 deletions(-) diff --git a/asdf/_node_info.py b/asdf/_node_info.py index f45ef3b13..18551ef6d 100644 --- a/asdf/_node_info.py +++ b/asdf/_node_info.py @@ -1,6 +1,7 @@ import re from collections import namedtuple +from .exceptions import AsdfSchemaResolutionError from .schema import load_schema from .treeutil import get_children @@ -20,6 +21,82 @@ def _filter_tree(info, filters): return len(info.children) > 0 or all(f(info.node, info.identifier) for f in filters) +def _get_matching_schema_property(schema, key): + if "properties" in schema: + props = schema["properties"] + if key in props: + return props[key] + if "patternProperties" in props: + patterns = props["patternProperties"] + for regex in patterns: + if re.search(regex, key): + return patterns[regex] + return None + + +def _get_subschema_for_property(schema, key): + # This does NOT handle $ref the expectation is that the schema + # is loaded with resolve_references=True + applicable = [] + + # first check properties and patternProperties + subschema = _get_matching_schema_property(schema, key) + if subschema is not None: + applicable.append(subschema) + + # next handle schema combiners + if "not" in schema: + # since we're only concerned here with if the schema applies + # it doesn't matter if the schema is nested in a not + subschema = _get_subschema_for_property(schema["not"], key) + if subschema is not None: + applicable.append(subschema) + + for combiner in ("allOf", "oneOf", "anyOf"): + for combined_schema in schema.get(combiner, []): + subschema = _get_subschema_for_property(combined_schema, key) + if subschema is not None: + applicable.append(subschema) + + if not applicable: + return None + + if len(applicable) > 1: + msg = ( + f"schema info could not be determined for {key} since " + f"{len(applicable)} possibly applicable schemas were found." + ) + raise AsdfSchemaResolutionError(msg) + + return applicable[0] + + +def _get_schema_key(schema, key): + applicable = [] + if key in schema: + applicable.append(schema[key]) + if "not" in schema: + possible = _get_schema_key(schema["not"], key) + if possible is not None: + applicable.append(possible) + for combiner in ("allOf", "oneOf", "anyOf"): + for combined_schema in schema.get(combiner, []): + possible = _get_schema_key(combined_schema, key) + if possible is not None: + applicable.append(possible) + if not applicable: + return None + + if len(applicable) > 1: + msg = ( + f"schema info could not be determined for {key} since " + f"{len(applicable)} possibly applicable schemas were found." + ) + raise AsdfSchemaResolutionError(msg) + + return applicable[0] + + def create_tree(key, node, identifier="root", filters=None, refresh_extension_manager=False): """ Create a `NodeSchemaInfo` tree which can be filtered from a base node. @@ -214,22 +291,12 @@ def parent_node(self): @property def info(self): - if self.schema is not None: - return self.schema.get(self.key, None) - - return None + if self.schema is None: + return None + return _get_schema_key(self.schema, self.key) def get_schema_for_property(self, identifier): - subschema = self.schema.get("properties", {}).get(identifier, None) - if subschema is not None: - return subschema - - subschema = self.schema.get("properties", {}).get("patternProperties", None) - if subschema: - for key in subschema: - if re.search(key, identifier): - return subschema[key] - return {} + return _get_subschema_for_property(self.schema, identifier) or {} def set_schema_for_property(self, parent, identifier): """Extract a subschema from the parent for the identified property""" @@ -241,7 +308,7 @@ def set_schema_from_node(self, node, extension_manager): tag_def = extension_manager.get_tag_definition(node._tag) schema_uri = tag_def.schema_uris[0] - schema = load_schema(schema_uri) + schema = load_schema(schema_uri, resolve_references=True) self.schema = schema diff --git a/asdf/_tests/test_info.py b/asdf/_tests/test_info.py index 94353051b..469bc1b45 100644 --- a/asdf/_tests/test_info.py +++ b/asdf/_tests/test_info.py @@ -168,8 +168,8 @@ def manifest_extension(tmp_path): description: Some silly description type: integer archive_catalog: - datatype: int - destination: [ScienceCommon.silly] + datatype: int + destination: [ScienceCommon.silly] clown: title: clown name description: clown description @@ -231,14 +231,14 @@ def manifest_extension(tmp_path): title: Attribute1 Title type: string archive_catalog: - datatype: str - destination: [ScienceCommon.attribute1] + datatype: str + destination: [ScienceCommon.attribute1] attribute2: title: Attribute2 Title type: string archive_catalog: - datatype: str - destination: [ScienceCommon.attribute2] + datatype: str + destination: [ScienceCommon.attribute2] ... """ @@ -251,19 +251,29 @@ def manifest_extension(tmp_path): type: object title: object with info support 3 title description: object description +allOf: + - $ref: drink_ref-1.0.0 +... +""" + drink_ref_schema = """ +%YAML 1.1 +--- +$schema: "asdf://stsci.edu/schemas/asdf/asdf-schema-1.1.0" +id: "asdf://somewhere.org/asdf/schemas/drink_ref-1.0.0" properties: attributeOne: title: AttributeOne Title description: AttributeOne description type: string archive_catalog: - datatype: str - destination: [ScienceCommon.attributeOne] + datatype: str + destination: [ScienceCommon.attributeOne] attributeTwo: - title: AttributeTwo Title - description: AttributeTwo description - type: string - archive_catalog: + allOf: + - title: AttributeTwo Title + description: AttributeTwo description + type: string + archive_catalog: datatype: str destination: [ScienceCommon.attributeTwo] ... @@ -278,6 +288,9 @@ def manifest_extension(tmp_path): spath = tmp_path / "schemas" / "drink-1.0.0.yaml" with open(spath, "w") as fschema: fschema.write(drink_schema) + spath = tmp_path / "schemas" / "drink_ref-1.0.0.yaml" + with open(spath, "w") as fschema: + fschema.write(drink_ref_schema) os.mkdir(tmp_path / "manifests") mpath = str(tmp_path / "manifests" / "foo_manifest-1.0.yaml") with open(mpath, "w") as fmanifest: diff --git a/asdf/exceptions.py b/asdf/exceptions.py index 0851a8baa..fefcc5e26 100644 --- a/asdf/exceptions.py +++ b/asdf/exceptions.py @@ -84,3 +84,9 @@ class AsdfSerializationError(RepresenterError): that the object does not have a supporting asdf Converter and needs to be manually converted to a supported type. """ + + +class AsdfSchemaResolutionError(ValueError): + """ + An attempt to lookup schema for an attribute failed. + """