From d3d05d9a69b261b8f266339ca22793a72faa3638 Mon Sep 17 00:00:00 2001 From: Darin Erat Sleiter Date: Thu, 13 May 2021 23:44:48 +1000 Subject: [PATCH 1/6] Validate builders against both top level data type specs and inner specs * Fix #585 * Ref #542 * Builder can now be validated against more than one spec in order to validate against additional fields added to inner data types * Also make validation Errors comparable as a way to remove duplicates that can sometimes be generated --- src/hdmf/validate/errors.py | 43 ++++++++++-- src/hdmf/validate/validator.py | 52 ++++++++++---- tests/unit/validator_tests/test_errors.py | 54 +++++++++++++++ tests/unit/validator_tests/test_validate.py | 75 +++++++++++++++++++++ 4 files changed, 205 insertions(+), 19 deletions(-) create mode 100644 tests/unit/validator_tests/test_errors.py diff --git a/src/hdmf/validate/errors.py b/src/hdmf/validate/errors.py index 1a6aeb904..fb1bfc1b4 100644 --- a/src/hdmf/validate/errors.py +++ b/src/hdmf/validate/errors.py @@ -25,10 +25,6 @@ def __init__(self, **kwargs): self.__name = getargs('name', kwargs) self.__reason = getargs('reason', kwargs) self.__location = getargs('location', kwargs) - if self.__location is not None: - self.__str = "%s (%s): %s" % (self.__name, self.__location, self.__reason) - else: - self.__str = "%s: %s" % (self.name, self.reason) @property def name(self): @@ -45,14 +41,49 @@ def location(self): @location.setter def location(self, loc): self.__location = loc - self.__str = "%s (%s): %s" % (self.__name, self.__location, self.__reason) def __str__(self): - return self.__str + return self.__format_str(self.name, self.location, self.reason) + + @staticmethod + def __format_str(name, location, reason): + if location is not None: + return "%s (%s): %s" % (name, location, reason) + else: + return "%s: %s" % (name, reason) def __repr__(self): return self.__str__() + def __hash__(self): + """Returns the hash value of this Error + + Note: if the location property is set after creation, the hash value will + change. Therefore, it is important to finalize the value of location + before getting the hash value. + """ + return hash(self.__equatable_str()) + + def __equatable_str(self): + """A string representation of the error which can be used to check for equality + + For a single error, name can end up being different depending on whether it is + generated from a base data type spec or from an inner type definition. These errors + should still be considered equal because they are caused by the same problem. + + When a location is provided, we only consider the name of the field and drop the + rest of the spec name. However, when a location is not available, then we need to + use the fully-provided name. + """ + if self.location is not None: + equatable_name = self.name.split('/')[-1] + else: + equatable_name = self.name + return self.__format_str(equatable_name, self.location, self.reason) + + def __eq__(self, other): + return hash(self) == hash(other) + class DtypeError(Error): diff --git a/src/hdmf/validate/validator.py b/src/hdmf/validate/validator.py index c2963e5ed..c0ad255ea 100644 --- a/src/hdmf/validate/validator.py +++ b/src/hdmf/validate/validator.py @@ -2,7 +2,7 @@ from abc import ABCMeta, abstractmethod from copy import copy from itertools import chain -from collections import defaultdict +from collections import defaultdict, OrderedDict import numpy as np @@ -418,7 +418,7 @@ def validate(self, **kwargs): # noqa: C901 errors = super().validate(builder) errors.extend(self.__validate_children(builder)) - return errors + return self._remove_duplicates(errors) def __validate_children(self, parent_builder): """Validates the children of the group builder against the children in the spec. @@ -491,8 +491,8 @@ def __validate_child_builder(self, child_spec, child_builder, parent_builder): yield self.__construct_illegal_link_error(child_spec, parent_builder) return # do not validate illegally linked objects child_builder = child_builder.builder - child_validator = self.__get_child_validator(child_spec) - yield from child_validator.validate(child_builder) + for child_validator in self.__get_child_validators(child_spec): + yield from child_validator.validate(child_builder) def __construct_illegal_link_error(self, child_spec, parent_builder): name_of_erroneous = self.get_spec_loc(child_spec) @@ -503,22 +503,48 @@ def __construct_illegal_link_error(self, child_spec, parent_builder): def __cannot_be_link(spec): return not isinstance(spec, LinkSpec) and not spec.linkable - def __get_child_validator(self, spec): - """Returns the appropriate validator for a child spec - - If a specific data type can be resolved, the validator is acquired from - the ValidatorMap, otherwise a new Validator is created. + def __get_child_validators(self, spec): + """Returns the appropriate list of validators for a child spec + + Due to the fact that child specs can both inherit a data type via data_type_inc + and also modify the type without defining a new data type via data_type_def, + we need to validate against both the spec for the base data type and the spec + at the current hierarchy of the data type in case there have been any + modifications. + + If a specific data type can be resolved, a validator for that type is acquired + from the ValidatorMap and included in the returned validators. If the spec is + a GroupSpec or a DatasetSpec, then a new Validator is created and also + returned. If the spec is a LinkSpec, no additional Validator is returned + because the LinkSpec cannot add or modify fields and the target_type will be + validated by the Validator returned from the ValidatorMap. """ if _resolve_data_type(spec) is not None: - return self.vmap.get_validator(_resolve_data_type(spec)) - elif isinstance(spec, GroupSpec): - return GroupValidator(spec, self.vmap) + yield self.vmap.get_validator(_resolve_data_type(spec)) + + if isinstance(spec, GroupSpec): + yield GroupValidator(spec, self.vmap) elif isinstance(spec, DatasetSpec): - return DatasetValidator(spec, self.vmap) + yield DatasetValidator(spec, self.vmap) + elif isinstance(spec, LinkSpec): + return else: msg = "Unable to resolve a validator for spec %s" % spec raise ValueError(msg) + @staticmethod + def _remove_duplicates(errors): + """Return a list of validation errors where duplicates have been removed + + In some cases a child of a group to be validated against two specs which can + redundantly define the same fields/children. If the builder doesn't match the + spec, it is possible for duplicate errors to be generated. + """ + ordered_errors = OrderedDict() + for error in errors: + ordered_errors[error] = error + return list(ordered_errors) + class SpecMatches: """A utility class to hold a spec and the builders matched to it""" diff --git a/tests/unit/validator_tests/test_errors.py b/tests/unit/validator_tests/test_errors.py new file mode 100644 index 000000000..3ae6aea8f --- /dev/null +++ b/tests/unit/validator_tests/test_errors.py @@ -0,0 +1,54 @@ +from unittest import TestCase + +from hdmf.validate.errors import Error + + +class TestErrorEquality(TestCase): + def test_self_equality(self): + """Verify that one error equals itself""" + error = Error('foo', 'bad thing', 'a.b.c') + self.assertEqual(error, error) + + def test_equality_with_same_field_values(self): + """Verify that two errors with the same field values are equal""" + err1 = Error('foo', 'bad thing', 'a.b.c') + err2 = Error('foo', 'bad thing', 'a.b.c') + self.assertEqual(err1, err2) + + def test_not_equal_with_different_reason(self): + """Verify that two errors with a different reason are not equal""" + err1 = Error('foo', 'bad thing', 'a.b.c') + err2 = Error('foo', 'something else', 'a.b.c') + self.assertNotEqual(err1, err2) + + def test_not_equal_with_different_name(self): + """Verify that two errors with a different name are not equal""" + err1 = Error('foo', 'bad thing', 'a.b.c') + err2 = Error('bar', 'bad thing', 'a.b.c') + self.assertNotEqual(err1, err2) + + def test_not_equal_with_different_location(self): + """Verify that two errors with a different location are not equal""" + err1 = Error('foo', 'bad thing', 'a.b.c') + err2 = Error('foo', 'bad thing', 'd.e.f') + self.assertNotEqual(err1, err2) + + def test_equal_with_no_location(self): + """Verify that two errors with no location but the same name are equal""" + err1 = Error('foo', 'bad thing') + err2 = Error('foo', 'bad thing') + self.assertEqual(err1, err2) + + def test_not_equal_with_overlapping_name_when_no_location(self): + """Verify that two errors with an overlapping name but no location are + not equal + """ + err1 = Error('foo', 'bad thing') + err2 = Error('x/y/foo', 'bad thing') + self.assertNotEqual(err1, err2) + + def test_equal_with_overlapping_name_when_location_present(self): + """Verify that two errors with an overlapping name and a location are equal""" + err1 = Error('foo', 'bad thing', 'a.b.c') + err2 = Error('x/y/foo', 'bad thing', 'a.b.c') + self.assertEqual(err1, err2) diff --git a/tests/unit/validator_tests/test_validate.py b/tests/unit/validator_tests/test_validate.py index b93dcdd55..05ca59ee7 100644 --- a/tests/unit/validator_tests/test_validate.py +++ b/tests/unit/validator_tests/test_validate.py @@ -821,3 +821,78 @@ def test_both_levels_of_hierarchy_validated_inverted_order(self): builder = GroupBuilder('my_baz', attributes={'data_type': 'Baz'}, datasets=datasets) result = self.vmap.validate(builder) self.assertEqual(len(result), 0) + + +class TestExtendedIncDataTypes(TestCase): + """Test validation against specs where a data type is included via data_type_inc + and modified by adding new fields or constraining existing files but is not + defined as a new type via data_type_inc. + + For the purpose of this test class: we are calling a data type which is nested + inside a group an "inner" data type. When an inner data inherits from a data type + via data_type_inc and has fields that are either added or modified from the base + data type, we are labeling that data type as an "extension". When the inner data + type extension does not define a new data type via data_type_def we say that it is + an "anonymous extension". + + Anonymous data type extensions should be avoided in for new specs, but + it does occur in existing nwb + specs, so we need to allow and validate against it. + One example is the `Units.spike_times` dataset attached to Units in the `core` + nwb namespace, which extends `VectorData` via neurodata_type_inc but adds a new + attribute named `resolution` without defining a new data type via neurodata_type_def. + """ + + def setup_spec(self): + """Prepare a set of specs for tests which includes an anonymous data type extension""" + spec_catalog = SpecCatalog() + attr_foo = AttributeSpec(name='foo', doc='an attribute', dtype='text') + attr_bar = AttributeSpec(name='bar', doc='an attribute', dtype='numeric') + d1_spec = DatasetSpec(doc='type D1', data_type_def='D1', dtype='numeric', + attributes=[attr_foo]) + d2_spec = DatasetSpec(doc='type D2', data_type_def='D2', data_type_inc=d1_spec) + g1_spec = GroupSpec(doc='type G1', data_type_def='G1', + datasets=[DatasetSpec(doc='D1 extension', data_type_inc=d1_spec, + attributes=[attr_foo, attr_bar])]) + for spec in [d1_spec, d2_spec, g1_spec]: + spec_catalog.register_spec(spec, 'test.yaml') + self.namespace = SpecNamespace('a test namespace', CORE_NAMESPACE, + [{'source': 'test.yaml'}], version='0.1.0', catalog=spec_catalog) + self.vmap = ValidatorMap(self.namespace) + + def test_missing_additional_attribute_on_anonymous_data_type_extension(self): + """Verify that a MissingError is returned when a required attribute from an + anonymous extension is not present + """ + self.setup_spec() + dataset = DatasetBuilder('test_d1', 42.0, attributes={'data_type': 'D1', 'foo': 'xyz'}) + builder = GroupBuilder('test_g1', attributes={'data_type': 'G1'}, datasets=[dataset]) + result = self.vmap.validate(builder) + self.assertEqual(len(result), 1) + error = result[0] + self.assertIsInstance(error, MissingError) + self.assertTrue('G1/D1/bar' in str(error)) + + def test_validate_child_type_against_anonymous_data_type_extension(self): + """Verify that a MissingError is returned when a required attribute from an + anonymous extension is not present on a data type which inherits from the data + type included in the anonymous extension. + """ + self.setup_spec() + dataset = DatasetBuilder('test_d2', 42.0, attributes={'data_type': 'D2', 'foo': 'xyz'}) + builder = GroupBuilder('test_g1', attributes={'data_type': 'G1'}, datasets=[dataset]) + result = self.vmap.validate(builder) + self.assertEqual(len(result), 1) + error = result[0] + self.assertIsInstance(error, MissingError) + self.assertTrue('G1/D1/bar' in str(error)) + + def test_redundant_attribute_in_spec(self): + """Test that only one MissingError is returned when an attribute is missing + which is redundantly defined in both a base data type and an inner data type + """ + self.setup_spec() + dataset = DatasetBuilder('test_d2', 42.0, attributes={'data_type': 'D2', 'bar': 5}) + builder = GroupBuilder('test_g1', attributes={'data_type': 'G1'}, datasets=[dataset]) + result = self.vmap.validate(builder) + self.assertEqual(len(result), 1) From eb1411f36b79f23c29c9b3fdcee50154d98280eb Mon Sep 17 00:00:00 2001 From: Darin Erat Sleiter Date: Fri, 14 May 2021 00:14:11 +1000 Subject: [PATCH 2/6] Update changelog * Ref #585 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e359a6880..baf09929b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # HDMF Changelog +## Upcoming Changes + +### Bug fix +- Update the validator to allow extensions to data types which only define data_type_inc @dsleiter (#609) + ## HDMF 2.5.3 (May 12, 2021) ### Bug fix From 9668b880f885876c813a44dd20c7e9a96facb47b Mon Sep 17 00:00:00 2001 From: Darin Erat Sleiter Date: Tue, 8 Jun 2021 23:28:37 +1000 Subject: [PATCH 3/6] Fix pynwb validation errors related to reference and compound data types * Ref #585 * This is just a workaround for checking the data_type of BuilderH5ReferenceDataset and BuilderH5TableDataset objects * Plan to add unit tests after some discussion to validate the approach --- src/hdmf/validate/validator.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/hdmf/validate/validator.py b/src/hdmf/validate/validator.py index c0ad255ea..a35b69755 100644 --- a/src/hdmf/validate/validator.py +++ b/src/hdmf/validate/validator.py @@ -15,6 +15,9 @@ from ..spec.spec import BaseStorageSpec, DtypeHelper from ..utils import docval, getargs, call_docval_func, pystr, get_data_shape +from hdmf.backends.hdf5.h5_utils import BuilderH5ReferenceDataset, BuilderH5TableDataset + + __synonyms = DtypeHelper.primary_dtype_synonyms __additional = { @@ -107,6 +110,8 @@ def get_type(data): return 'region' elif isinstance(data, ReferenceBuilder): return 'object' + elif isinstance(data, BuilderH5ReferenceDataset): + return 'object' elif isinstance(data, np.ndarray): if data.size == 0: raise EmptyArrayError() @@ -117,6 +122,8 @@ def get_type(data): return type(data).__name__ else: if hasattr(data, 'dtype'): + if isinstance(data, BuilderH5TableDataset): + return data.dtype if isinstance(data.dtype, list): return [get_type(data[0][i]) for i in range(len(data.dtype))] if data.dtype.metadata is not None and data.dtype.metadata.get('vlen') is not None: From 1ec83d627457b98431d7e67cd779c9b72fe0b444 Mon Sep 17 00:00:00 2001 From: Darin Erat Sleiter Date: Thu, 17 Jun 2021 16:57:18 +1000 Subject: [PATCH 4/6] Remove validator reference to H5-specific classes and add unit tests * use ReferenceResolver instead of referencing BuilderH5ReferenceDataset or BuilderH5TableDataset * Fix #585 --- src/hdmf/validate/validator.py | 9 +- tests/unit/validator_tests/test_validate.py | 119 +++++++++++++++++++- 2 files changed, 119 insertions(+), 9 deletions(-) diff --git a/src/hdmf/validate/validator.py b/src/hdmf/validate/validator.py index a35b69755..9dcecbb78 100644 --- a/src/hdmf/validate/validator.py +++ b/src/hdmf/validate/validator.py @@ -14,8 +14,7 @@ from ..spec import SpecNamespace from ..spec.spec import BaseStorageSpec, DtypeHelper from ..utils import docval, getargs, call_docval_func, pystr, get_data_shape - -from hdmf.backends.hdf5.h5_utils import BuilderH5ReferenceDataset, BuilderH5TableDataset +from ..query import ReferenceResolver __synonyms = DtypeHelper.primary_dtype_synonyms @@ -110,8 +109,8 @@ def get_type(data): return 'region' elif isinstance(data, ReferenceBuilder): return 'object' - elif isinstance(data, BuilderH5ReferenceDataset): - return 'object' + elif isinstance(data, ReferenceResolver): + return data.dtype elif isinstance(data, np.ndarray): if data.size == 0: raise EmptyArrayError() @@ -122,8 +121,6 @@ def get_type(data): return type(data).__name__ else: if hasattr(data, 'dtype'): - if isinstance(data, BuilderH5TableDataset): - return data.dtype if isinstance(data.dtype, list): return [get_type(data[0][i]) for i in range(len(data.dtype))] if data.dtype.metadata is not None and data.dtype.metadata.get('vlen') is not None: diff --git a/tests/unit/validator_tests/test_validate.py b/tests/unit/validator_tests/test_validate.py index 05ca59ee7..29f56b9d7 100644 --- a/tests/unit/validator_tests/test_validate.py +++ b/tests/unit/validator_tests/test_validate.py @@ -4,13 +4,15 @@ import numpy as np from dateutil.tz import tzlocal -from hdmf.build import GroupBuilder, DatasetBuilder, LinkBuilder -from hdmf.spec import GroupSpec, AttributeSpec, DatasetSpec, SpecCatalog, SpecNamespace, LinkSpec +from hdmf.build import GroupBuilder, DatasetBuilder, LinkBuilder, ReferenceBuilder, TypeMap, BuildManager +from hdmf.spec import (GroupSpec, AttributeSpec, DatasetSpec, SpecCatalog, SpecNamespace, + LinkSpec, RefSpec, NamespaceCatalog, DtypeSpec) from hdmf.spec.spec import ONE_OR_MANY, ZERO_OR_MANY, ZERO_OR_ONE -from hdmf.testing import TestCase +from hdmf.testing import TestCase, remove_test_file from hdmf.validate import ValidatorMap from hdmf.validate.errors import (DtypeError, MissingError, ExpectedArrayError, MissingDataType, IncorrectQuantityError, IllegalLinkError) +from hdmf.backends.hdf5 import HDF5IO CORE_NAMESPACE = 'test_core' @@ -896,3 +898,114 @@ def test_redundant_attribute_in_spec(self): builder = GroupBuilder('test_g1', attributes={'data_type': 'G1'}, datasets=[dataset]) result = self.vmap.validate(builder) self.assertEqual(len(result), 1) + + +class TestReferenceDatasetsRoundTrip(ValidatorTestBase): + """Test that no errors occur when when datasets containing references either in an + array or as part of a compound type are written out to file, read back in, and + then validated. + + In order to support lazy reading on loading, datasets containing references are + wrapped in lazy-loading ReferenceResolver objects. These tests verify that the + validator can work with these ReferenceResolver objects. + """ + + def setUp(self): + self.filename = 'test_ref_dataset.h5' + super().setUp() + + def tearDown(self): + remove_test_file(self.filename) + super().tearDown() + + def getSpecs(self): + qux_spec = DatasetSpec( + doc='a simple scalar dataset', + data_type_def='Qux', + dtype='int', + shape=None + ) + baz_spec = DatasetSpec( + doc='a dataset with a compound datatype that includes a reference', + data_type_def='Baz', + dtype=[ + DtypeSpec('x', doc='x-value', dtype='int'), + DtypeSpec('y', doc='y-ref', dtype=RefSpec('Qux', reftype='object')) + ], + shape=None + ) + bar_spec = DatasetSpec( + doc='a dataset of an array of references', + dtype=RefSpec('Qux', reftype='object'), + data_type_def='Bar', + shape=(None,) + ) + foo_spec = GroupSpec( + doc='a base group for containing test datasets', + data_type_def='Foo', + datasets=[ + DatasetSpec(doc='optional Bar', data_type_inc=bar_spec, quantity=ZERO_OR_ONE), + DatasetSpec(doc='optional Baz', data_type_inc=baz_spec, quantity=ZERO_OR_ONE), + DatasetSpec(doc='multiple qux', data_type_inc=qux_spec, quantity=ONE_OR_MANY) + ] + ) + return (foo_spec, bar_spec, baz_spec, qux_spec) + + def runBuilderRoundTrip(self, builder): + """Executes a round-trip test for a builder + + 1. First writes the builder to file, + 2. next reads a new builder from disk + 3. and finally runs the builder through the validator. + The test is successful if there are no validation errors.""" + ns_catalog = NamespaceCatalog() + ns_catalog.add_namespace(self.namespace.name, self.namespace) + typemap = TypeMap(ns_catalog) + self.manager = BuildManager(typemap) + + with HDF5IO(self.filename, manager=self.manager, mode='w') as write_io: + write_io.write_builder(builder) + + with HDF5IO(self.filename, manager=self.manager, mode='r') as read_io: + read_builder = read_io.read_builder() + errors = self.vmap.validate(read_builder) + self.assertEqual(len(errors), 0, errors) + + def test_round_trip_validation_of_reference_dataset_array(self): + """Verify that a dataset builder containing an array of references passes + validation after a round trip""" + qux1 = DatasetBuilder('q1', 5, attributes={'data_type': 'Qux'}) + qux2 = DatasetBuilder('q2', 10, attributes={'data_type': 'Qux'}) + bar = DatasetBuilder( + name='bar', + data=[ReferenceBuilder(qux1), ReferenceBuilder(qux2)], + attributes={'data_type': 'Bar'}, + dtype='object' + ) + foo = GroupBuilder( + name='foo', + datasets=[bar, qux1, qux2], + attributes={'data_type': 'Foo'} + ) + self.runBuilderRoundTrip(foo) + + def test_round_trip_validation_of_compound_dtype_with_reference(self): + """Verify that a dataset builder containing data with a compound dtype + containing a reference passes validation after a round trip""" + qux1 = DatasetBuilder('q1', 5, attributes={'data_type': 'Qux'}) + qux2 = DatasetBuilder('q2', 10, attributes={'data_type': 'Qux'}) + baz = DatasetBuilder( + name='baz', + data=[(10, ReferenceBuilder(qux1))], + dtype=[ + DtypeSpec('x', doc='x-value', dtype='int'), + DtypeSpec('y', doc='y-ref', dtype=RefSpec('Qux', reftype='object')) + ], + attributes={'data_type': 'Baz'} + ) + foo = GroupBuilder( + name='foo', + datasets=[baz, qux1, qux2], + attributes={'data_type': 'Foo'} + ) + self.runBuilderRoundTrip(foo) From f1ef930155241ec7a4e56d6d4c6f0d705faa6be0 Mon Sep 17 00:00:00 2001 From: Ryan Ly Date: Thu, 24 Jun 2021 16:23:27 -0700 Subject: [PATCH 5/6] Update tests/unit/validator_tests/test_validate.py --- tests/unit/validator_tests/test_validate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/validator_tests/test_validate.py b/tests/unit/validator_tests/test_validate.py index 29f56b9d7..9e6b11a67 100644 --- a/tests/unit/validator_tests/test_validate.py +++ b/tests/unit/validator_tests/test_validate.py @@ -827,7 +827,7 @@ def test_both_levels_of_hierarchy_validated_inverted_order(self): class TestExtendedIncDataTypes(TestCase): """Test validation against specs where a data type is included via data_type_inc - and modified by adding new fields or constraining existing files but is not + and modified by adding new fields or constraining existing fields but is not defined as a new type via data_type_inc. For the purpose of this test class: we are calling a data type which is nested From 29829c8ba058a0386e6ce0eda5480d5933ae9d9e Mon Sep 17 00:00:00 2001 From: Ryan Ly Date: Thu, 24 Jun 2021 16:41:58 -0700 Subject: [PATCH 6/6] Update tests/unit/validator_tests/test_validate.py --- tests/unit/validator_tests/test_validate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/validator_tests/test_validate.py b/tests/unit/validator_tests/test_validate.py index 9e6b11a67..bc091c317 100644 --- a/tests/unit/validator_tests/test_validate.py +++ b/tests/unit/validator_tests/test_validate.py @@ -831,7 +831,7 @@ class TestExtendedIncDataTypes(TestCase): defined as a new type via data_type_inc. For the purpose of this test class: we are calling a data type which is nested - inside a group an "inner" data type. When an inner data inherits from a data type + inside a group an "inner" data type. When an inner data type inherits from a data type via data_type_inc and has fields that are either added or modified from the base data type, we are labeling that data type as an "extension". When the inner data type extension does not define a new data type via data_type_def we say that it is