From 92d28baedda7d2030e9a57e6a6b2df1c0b2b017f Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Wed, 7 Aug 2024 20:23:18 -0700 Subject: [PATCH] coercion for extra columns passed as arrays --- nwb_linkml/src/nwb_linkml/includes/hdmf.py | 40 ++++++++++++++++--- .../hdmf_common/v1_1_0/hdmf_common_table.py | 40 ++++++++++++++++--- .../hdmf_common/v1_1_2/hdmf_common_table.py | 40 ++++++++++++++++--- .../hdmf_common/v1_1_3/hdmf_common_table.py | 40 ++++++++++++++++--- .../hdmf_common/v1_2_0/hdmf_common_table.py | 40 ++++++++++++++++--- .../hdmf_common/v1_2_1/hdmf_common_table.py | 40 ++++++++++++++++--- .../hdmf_common/v1_3_0/hdmf_common_table.py | 40 ++++++++++++++++--- .../hdmf_common/v1_4_0/hdmf_common_table.py | 40 ++++++++++++++++--- .../hdmf_common/v1_5_0/hdmf_common_table.py | 40 ++++++++++++++++--- .../hdmf_common/v1_5_1/hdmf_common_table.py | 40 ++++++++++++++++--- .../hdmf_common/v1_6_0/hdmf_common_table.py | 40 ++++++++++++++++--- .../hdmf_common/v1_7_0/hdmf_common_table.py | 40 ++++++++++++++++--- .../hdmf_common/v1_8_0/hdmf_common_table.py | 40 ++++++++++++++++--- nwb_linkml/tests/test_includes/test_hdmf.py | 9 ++++- 14 files changed, 463 insertions(+), 66 deletions(-) diff --git a/nwb_linkml/src/nwb_linkml/includes/hdmf.py b/nwb_linkml/src/nwb_linkml/includes/hdmf.py index 1e0c3f7..c34bf9a 100644 --- a/nwb_linkml/src/nwb_linkml/includes/hdmf.py +++ b/nwb_linkml/src/nwb_linkml/includes/hdmf.py @@ -227,6 +227,32 @@ class DynamicTableMixin(BaseModel): model["colnames"].extend(colnames) return model + @model_validator(mode="after") + def cast_extra_columns(self): + """ + If extra columns are passed as just lists or arrays, cast to VectorData + before we resolve targets for VectorData and VectorIndex pairs. + + See :meth:`.cast_specified_columns` for handling columns in the class specification + """ + # if columns are not in the specification, cast to a generic VectorData + for key, val in self.__pydantic_extra__.items(): + if not isinstance(val, (VectorData, VectorIndex)): + try: + if key.endswith("_index"): + self.__pydantic_extra__[key] = VectorIndex( + name=key, description="", value=val + ) + else: + self.__pydantic_extra__[key] = VectorData( + name=key, description="", value=val + ) + except ValidationError as e: + raise ValidationError( + f"field {key} cannot be cast to VectorData from {val}" + ) from e + return self + @model_validator(mode="after") def resolve_targets(self) -> "DynamicTableMixin": """ @@ -263,11 +289,15 @@ class DynamicTableMixin(BaseModel): @field_validator("*", mode="wrap") @classmethod - def cast_columns( + def cast_specified_columns( cls, val: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo ) -> Any: """ - If columns are supplied as arrays, try casting them to the type before validating + If columns *in* the model specification are supplied as arrays, + try casting them to the type before validating. + + Columns that are not in the spec are handled separately in + :meth:`.cast_extra_columns` """ try: return handler(val) @@ -361,7 +391,7 @@ class VectorIndexMixin(BaseModel): if self.target is None: return self.value[item] else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self._getitem_helper(item) elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -412,7 +442,7 @@ class DynamicTableRegionMixin(BaseModel): this being a subclass of ``VectorData`` """ if self._index: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): # index returns an array of indices, # and indexing table with an array returns a list of rows return self.table[self._index[item]] @@ -424,7 +454,7 @@ class DynamicTableRegionMixin(BaseModel): else: raise ValueError(f"Dont know how to index with {item}, need an int or a slice") else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self.table[self.value[item]] elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): diff --git a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_1_0/hdmf_common_table.py b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_1_0/hdmf_common_table.py index 41ca9bd..1c07ed3 100644 --- a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_1_0/hdmf_common_table.py +++ b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_1_0/hdmf_common_table.py @@ -136,7 +136,7 @@ class VectorIndexMixin(BaseModel): if self.target is None: return self.value[item] else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self._getitem_helper(item) elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -187,7 +187,7 @@ class DynamicTableRegionMixin(BaseModel): this being a subclass of ``VectorData`` """ if self._index: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): # index returns an array of indices, # and indexing table with an array returns a list of rows return self.table[self._index[item]] @@ -199,7 +199,7 @@ class DynamicTableRegionMixin(BaseModel): else: raise ValueError(f"Dont know how to index with {item}, need an int or a slice") else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self.table[self.value[item]] elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -405,6 +405,32 @@ class DynamicTableMixin(BaseModel): model["colnames"].extend(colnames) return model + @model_validator(mode="after") + def cast_extra_columns(self): + """ + If extra columns are passed as just lists or arrays, cast to VectorData + before we resolve targets for VectorData and VectorIndex pairs. + + See :meth:`.cast_specified_columns` for handling columns in the class specification + """ + # if columns are not in the specification, cast to a generic VectorData + for key, val in self.__pydantic_extra__.items(): + if not isinstance(val, (VectorData, VectorIndex)): + try: + if key.endswith("_index"): + self.__pydantic_extra__[key] = VectorIndex( + name=key, description="", value=val + ) + else: + self.__pydantic_extra__[key] = VectorData( + name=key, description="", value=val + ) + except ValidationError as e: + raise ValidationError( + f"field {key} cannot be cast to VectorData from {val}" + ) from e + return self + @model_validator(mode="after") def resolve_targets(self) -> "DynamicTableMixin": """ @@ -441,11 +467,15 @@ class DynamicTableMixin(BaseModel): @field_validator("*", mode="wrap") @classmethod - def cast_columns( + def cast_specified_columns( cls, val: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo ) -> Any: """ - If columns are supplied as arrays, try casting them to the type before validating + If columns *in* the model specification are supplied as arrays, + try casting them to the type before validating. + + Columns that are not in the spec are handled separately in + :meth:`.cast_extra_columns` """ try: return handler(val) diff --git a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_1_2/hdmf_common_table.py b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_1_2/hdmf_common_table.py index 927d9c0..36a79bb 100644 --- a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_1_2/hdmf_common_table.py +++ b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_1_2/hdmf_common_table.py @@ -136,7 +136,7 @@ class VectorIndexMixin(BaseModel): if self.target is None: return self.value[item] else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self._getitem_helper(item) elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -187,7 +187,7 @@ class DynamicTableRegionMixin(BaseModel): this being a subclass of ``VectorData`` """ if self._index: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): # index returns an array of indices, # and indexing table with an array returns a list of rows return self.table[self._index[item]] @@ -199,7 +199,7 @@ class DynamicTableRegionMixin(BaseModel): else: raise ValueError(f"Dont know how to index with {item}, need an int or a slice") else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self.table[self.value[item]] elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -405,6 +405,32 @@ class DynamicTableMixin(BaseModel): model["colnames"].extend(colnames) return model + @model_validator(mode="after") + def cast_extra_columns(self): + """ + If extra columns are passed as just lists or arrays, cast to VectorData + before we resolve targets for VectorData and VectorIndex pairs. + + See :meth:`.cast_specified_columns` for handling columns in the class specification + """ + # if columns are not in the specification, cast to a generic VectorData + for key, val in self.__pydantic_extra__.items(): + if not isinstance(val, (VectorData, VectorIndex)): + try: + if key.endswith("_index"): + self.__pydantic_extra__[key] = VectorIndex( + name=key, description="", value=val + ) + else: + self.__pydantic_extra__[key] = VectorData( + name=key, description="", value=val + ) + except ValidationError as e: + raise ValidationError( + f"field {key} cannot be cast to VectorData from {val}" + ) from e + return self + @model_validator(mode="after") def resolve_targets(self) -> "DynamicTableMixin": """ @@ -441,11 +467,15 @@ class DynamicTableMixin(BaseModel): @field_validator("*", mode="wrap") @classmethod - def cast_columns( + def cast_specified_columns( cls, val: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo ) -> Any: """ - If columns are supplied as arrays, try casting them to the type before validating + If columns *in* the model specification are supplied as arrays, + try casting them to the type before validating. + + Columns that are not in the spec are handled separately in + :meth:`.cast_extra_columns` """ try: return handler(val) diff --git a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_1_3/hdmf_common_table.py b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_1_3/hdmf_common_table.py index 01324a9..a5477d8 100644 --- a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_1_3/hdmf_common_table.py +++ b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_1_3/hdmf_common_table.py @@ -136,7 +136,7 @@ class VectorIndexMixin(BaseModel): if self.target is None: return self.value[item] else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self._getitem_helper(item) elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -187,7 +187,7 @@ class DynamicTableRegionMixin(BaseModel): this being a subclass of ``VectorData`` """ if self._index: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): # index returns an array of indices, # and indexing table with an array returns a list of rows return self.table[self._index[item]] @@ -199,7 +199,7 @@ class DynamicTableRegionMixin(BaseModel): else: raise ValueError(f"Dont know how to index with {item}, need an int or a slice") else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self.table[self.value[item]] elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -405,6 +405,32 @@ class DynamicTableMixin(BaseModel): model["colnames"].extend(colnames) return model + @model_validator(mode="after") + def cast_extra_columns(self): + """ + If extra columns are passed as just lists or arrays, cast to VectorData + before we resolve targets for VectorData and VectorIndex pairs. + + See :meth:`.cast_specified_columns` for handling columns in the class specification + """ + # if columns are not in the specification, cast to a generic VectorData + for key, val in self.__pydantic_extra__.items(): + if not isinstance(val, (VectorData, VectorIndex)): + try: + if key.endswith("_index"): + self.__pydantic_extra__[key] = VectorIndex( + name=key, description="", value=val + ) + else: + self.__pydantic_extra__[key] = VectorData( + name=key, description="", value=val + ) + except ValidationError as e: + raise ValidationError( + f"field {key} cannot be cast to VectorData from {val}" + ) from e + return self + @model_validator(mode="after") def resolve_targets(self) -> "DynamicTableMixin": """ @@ -441,11 +467,15 @@ class DynamicTableMixin(BaseModel): @field_validator("*", mode="wrap") @classmethod - def cast_columns( + def cast_specified_columns( cls, val: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo ) -> Any: """ - If columns are supplied as arrays, try casting them to the type before validating + If columns *in* the model specification are supplied as arrays, + try casting them to the type before validating. + + Columns that are not in the spec are handled separately in + :meth:`.cast_extra_columns` """ try: return handler(val) diff --git a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_2_0/hdmf_common_table.py b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_2_0/hdmf_common_table.py index f9f4450..bd03453 100644 --- a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_2_0/hdmf_common_table.py +++ b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_2_0/hdmf_common_table.py @@ -137,7 +137,7 @@ class VectorIndexMixin(BaseModel): if self.target is None: return self.value[item] else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self._getitem_helper(item) elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -188,7 +188,7 @@ class DynamicTableRegionMixin(BaseModel): this being a subclass of ``VectorData`` """ if self._index: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): # index returns an array of indices, # and indexing table with an array returns a list of rows return self.table[self._index[item]] @@ -200,7 +200,7 @@ class DynamicTableRegionMixin(BaseModel): else: raise ValueError(f"Dont know how to index with {item}, need an int or a slice") else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self.table[self.value[item]] elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -406,6 +406,32 @@ class DynamicTableMixin(BaseModel): model["colnames"].extend(colnames) return model + @model_validator(mode="after") + def cast_extra_columns(self): + """ + If extra columns are passed as just lists or arrays, cast to VectorData + before we resolve targets for VectorData and VectorIndex pairs. + + See :meth:`.cast_specified_columns` for handling columns in the class specification + """ + # if columns are not in the specification, cast to a generic VectorData + for key, val in self.__pydantic_extra__.items(): + if not isinstance(val, (VectorData, VectorIndex)): + try: + if key.endswith("_index"): + self.__pydantic_extra__[key] = VectorIndex( + name=key, description="", value=val + ) + else: + self.__pydantic_extra__[key] = VectorData( + name=key, description="", value=val + ) + except ValidationError as e: + raise ValidationError( + f"field {key} cannot be cast to VectorData from {val}" + ) from e + return self + @model_validator(mode="after") def resolve_targets(self) -> "DynamicTableMixin": """ @@ -442,11 +468,15 @@ class DynamicTableMixin(BaseModel): @field_validator("*", mode="wrap") @classmethod - def cast_columns( + def cast_specified_columns( cls, val: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo ) -> Any: """ - If columns are supplied as arrays, try casting them to the type before validating + If columns *in* the model specification are supplied as arrays, + try casting them to the type before validating. + + Columns that are not in the spec are handled separately in + :meth:`.cast_extra_columns` """ try: return handler(val) diff --git a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_2_1/hdmf_common_table.py b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_2_1/hdmf_common_table.py index e297747..82c84bf 100644 --- a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_2_1/hdmf_common_table.py +++ b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_2_1/hdmf_common_table.py @@ -137,7 +137,7 @@ class VectorIndexMixin(BaseModel): if self.target is None: return self.value[item] else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self._getitem_helper(item) elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -188,7 +188,7 @@ class DynamicTableRegionMixin(BaseModel): this being a subclass of ``VectorData`` """ if self._index: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): # index returns an array of indices, # and indexing table with an array returns a list of rows return self.table[self._index[item]] @@ -200,7 +200,7 @@ class DynamicTableRegionMixin(BaseModel): else: raise ValueError(f"Dont know how to index with {item}, need an int or a slice") else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self.table[self.value[item]] elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -406,6 +406,32 @@ class DynamicTableMixin(BaseModel): model["colnames"].extend(colnames) return model + @model_validator(mode="after") + def cast_extra_columns(self): + """ + If extra columns are passed as just lists or arrays, cast to VectorData + before we resolve targets for VectorData and VectorIndex pairs. + + See :meth:`.cast_specified_columns` for handling columns in the class specification + """ + # if columns are not in the specification, cast to a generic VectorData + for key, val in self.__pydantic_extra__.items(): + if not isinstance(val, (VectorData, VectorIndex)): + try: + if key.endswith("_index"): + self.__pydantic_extra__[key] = VectorIndex( + name=key, description="", value=val + ) + else: + self.__pydantic_extra__[key] = VectorData( + name=key, description="", value=val + ) + except ValidationError as e: + raise ValidationError( + f"field {key} cannot be cast to VectorData from {val}" + ) from e + return self + @model_validator(mode="after") def resolve_targets(self) -> "DynamicTableMixin": """ @@ -442,11 +468,15 @@ class DynamicTableMixin(BaseModel): @field_validator("*", mode="wrap") @classmethod - def cast_columns( + def cast_specified_columns( cls, val: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo ) -> Any: """ - If columns are supplied as arrays, try casting them to the type before validating + If columns *in* the model specification are supplied as arrays, + try casting them to the type before validating. + + Columns that are not in the spec are handled separately in + :meth:`.cast_extra_columns` """ try: return handler(val) diff --git a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_3_0/hdmf_common_table.py b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_3_0/hdmf_common_table.py index 50eeb23..23f75ee 100644 --- a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_3_0/hdmf_common_table.py +++ b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_3_0/hdmf_common_table.py @@ -137,7 +137,7 @@ class VectorIndexMixin(BaseModel): if self.target is None: return self.value[item] else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self._getitem_helper(item) elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -188,7 +188,7 @@ class DynamicTableRegionMixin(BaseModel): this being a subclass of ``VectorData`` """ if self._index: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): # index returns an array of indices, # and indexing table with an array returns a list of rows return self.table[self._index[item]] @@ -200,7 +200,7 @@ class DynamicTableRegionMixin(BaseModel): else: raise ValueError(f"Dont know how to index with {item}, need an int or a slice") else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self.table[self.value[item]] elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -406,6 +406,32 @@ class DynamicTableMixin(BaseModel): model["colnames"].extend(colnames) return model + @model_validator(mode="after") + def cast_extra_columns(self): + """ + If extra columns are passed as just lists or arrays, cast to VectorData + before we resolve targets for VectorData and VectorIndex pairs. + + See :meth:`.cast_specified_columns` for handling columns in the class specification + """ + # if columns are not in the specification, cast to a generic VectorData + for key, val in self.__pydantic_extra__.items(): + if not isinstance(val, (VectorData, VectorIndex)): + try: + if key.endswith("_index"): + self.__pydantic_extra__[key] = VectorIndex( + name=key, description="", value=val + ) + else: + self.__pydantic_extra__[key] = VectorData( + name=key, description="", value=val + ) + except ValidationError as e: + raise ValidationError( + f"field {key} cannot be cast to VectorData from {val}" + ) from e + return self + @model_validator(mode="after") def resolve_targets(self) -> "DynamicTableMixin": """ @@ -442,11 +468,15 @@ class DynamicTableMixin(BaseModel): @field_validator("*", mode="wrap") @classmethod - def cast_columns( + def cast_specified_columns( cls, val: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo ) -> Any: """ - If columns are supplied as arrays, try casting them to the type before validating + If columns *in* the model specification are supplied as arrays, + try casting them to the type before validating. + + Columns that are not in the spec are handled separately in + :meth:`.cast_extra_columns` """ try: return handler(val) diff --git a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_4_0/hdmf_common_table.py b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_4_0/hdmf_common_table.py index affd5dc..e5d4abc 100644 --- a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_4_0/hdmf_common_table.py +++ b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_4_0/hdmf_common_table.py @@ -137,7 +137,7 @@ class VectorIndexMixin(BaseModel): if self.target is None: return self.value[item] else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self._getitem_helper(item) elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -188,7 +188,7 @@ class DynamicTableRegionMixin(BaseModel): this being a subclass of ``VectorData`` """ if self._index: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): # index returns an array of indices, # and indexing table with an array returns a list of rows return self.table[self._index[item]] @@ -200,7 +200,7 @@ class DynamicTableRegionMixin(BaseModel): else: raise ValueError(f"Dont know how to index with {item}, need an int or a slice") else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self.table[self.value[item]] elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -406,6 +406,32 @@ class DynamicTableMixin(BaseModel): model["colnames"].extend(colnames) return model + @model_validator(mode="after") + def cast_extra_columns(self): + """ + If extra columns are passed as just lists or arrays, cast to VectorData + before we resolve targets for VectorData and VectorIndex pairs. + + See :meth:`.cast_specified_columns` for handling columns in the class specification + """ + # if columns are not in the specification, cast to a generic VectorData + for key, val in self.__pydantic_extra__.items(): + if not isinstance(val, (VectorData, VectorIndex)): + try: + if key.endswith("_index"): + self.__pydantic_extra__[key] = VectorIndex( + name=key, description="", value=val + ) + else: + self.__pydantic_extra__[key] = VectorData( + name=key, description="", value=val + ) + except ValidationError as e: + raise ValidationError( + f"field {key} cannot be cast to VectorData from {val}" + ) from e + return self + @model_validator(mode="after") def resolve_targets(self) -> "DynamicTableMixin": """ @@ -442,11 +468,15 @@ class DynamicTableMixin(BaseModel): @field_validator("*", mode="wrap") @classmethod - def cast_columns( + def cast_specified_columns( cls, val: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo ) -> Any: """ - If columns are supplied as arrays, try casting them to the type before validating + If columns *in* the model specification are supplied as arrays, + try casting them to the type before validating. + + Columns that are not in the spec are handled separately in + :meth:`.cast_extra_columns` """ try: return handler(val) diff --git a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_5_0/hdmf_common_table.py b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_5_0/hdmf_common_table.py index 5b99f2c..46796a1 100644 --- a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_5_0/hdmf_common_table.py +++ b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_5_0/hdmf_common_table.py @@ -137,7 +137,7 @@ class VectorIndexMixin(BaseModel): if self.target is None: return self.value[item] else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self._getitem_helper(item) elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -188,7 +188,7 @@ class DynamicTableRegionMixin(BaseModel): this being a subclass of ``VectorData`` """ if self._index: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): # index returns an array of indices, # and indexing table with an array returns a list of rows return self.table[self._index[item]] @@ -200,7 +200,7 @@ class DynamicTableRegionMixin(BaseModel): else: raise ValueError(f"Dont know how to index with {item}, need an int or a slice") else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self.table[self.value[item]] elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -406,6 +406,32 @@ class DynamicTableMixin(BaseModel): model["colnames"].extend(colnames) return model + @model_validator(mode="after") + def cast_extra_columns(self): + """ + If extra columns are passed as just lists or arrays, cast to VectorData + before we resolve targets for VectorData and VectorIndex pairs. + + See :meth:`.cast_specified_columns` for handling columns in the class specification + """ + # if columns are not in the specification, cast to a generic VectorData + for key, val in self.__pydantic_extra__.items(): + if not isinstance(val, (VectorData, VectorIndex)): + try: + if key.endswith("_index"): + self.__pydantic_extra__[key] = VectorIndex( + name=key, description="", value=val + ) + else: + self.__pydantic_extra__[key] = VectorData( + name=key, description="", value=val + ) + except ValidationError as e: + raise ValidationError( + f"field {key} cannot be cast to VectorData from {val}" + ) from e + return self + @model_validator(mode="after") def resolve_targets(self) -> "DynamicTableMixin": """ @@ -442,11 +468,15 @@ class DynamicTableMixin(BaseModel): @field_validator("*", mode="wrap") @classmethod - def cast_columns( + def cast_specified_columns( cls, val: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo ) -> Any: """ - If columns are supplied as arrays, try casting them to the type before validating + If columns *in* the model specification are supplied as arrays, + try casting them to the type before validating. + + Columns that are not in the spec are handled separately in + :meth:`.cast_extra_columns` """ try: return handler(val) diff --git a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_5_1/hdmf_common_table.py b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_5_1/hdmf_common_table.py index 2eb4675..9880ee8 100644 --- a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_5_1/hdmf_common_table.py +++ b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_5_1/hdmf_common_table.py @@ -137,7 +137,7 @@ class VectorIndexMixin(BaseModel): if self.target is None: return self.value[item] else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self._getitem_helper(item) elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -188,7 +188,7 @@ class DynamicTableRegionMixin(BaseModel): this being a subclass of ``VectorData`` """ if self._index: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): # index returns an array of indices, # and indexing table with an array returns a list of rows return self.table[self._index[item]] @@ -200,7 +200,7 @@ class DynamicTableRegionMixin(BaseModel): else: raise ValueError(f"Dont know how to index with {item}, need an int or a slice") else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self.table[self.value[item]] elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -406,6 +406,32 @@ class DynamicTableMixin(BaseModel): model["colnames"].extend(colnames) return model + @model_validator(mode="after") + def cast_extra_columns(self): + """ + If extra columns are passed as just lists or arrays, cast to VectorData + before we resolve targets for VectorData and VectorIndex pairs. + + See :meth:`.cast_specified_columns` for handling columns in the class specification + """ + # if columns are not in the specification, cast to a generic VectorData + for key, val in self.__pydantic_extra__.items(): + if not isinstance(val, (VectorData, VectorIndex)): + try: + if key.endswith("_index"): + self.__pydantic_extra__[key] = VectorIndex( + name=key, description="", value=val + ) + else: + self.__pydantic_extra__[key] = VectorData( + name=key, description="", value=val + ) + except ValidationError as e: + raise ValidationError( + f"field {key} cannot be cast to VectorData from {val}" + ) from e + return self + @model_validator(mode="after") def resolve_targets(self) -> "DynamicTableMixin": """ @@ -442,11 +468,15 @@ class DynamicTableMixin(BaseModel): @field_validator("*", mode="wrap") @classmethod - def cast_columns( + def cast_specified_columns( cls, val: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo ) -> Any: """ - If columns are supplied as arrays, try casting them to the type before validating + If columns *in* the model specification are supplied as arrays, + try casting them to the type before validating. + + Columns that are not in the spec are handled separately in + :meth:`.cast_extra_columns` """ try: return handler(val) diff --git a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_6_0/hdmf_common_table.py b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_6_0/hdmf_common_table.py index d578633..158f8c1 100644 --- a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_6_0/hdmf_common_table.py +++ b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_6_0/hdmf_common_table.py @@ -137,7 +137,7 @@ class VectorIndexMixin(BaseModel): if self.target is None: return self.value[item] else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self._getitem_helper(item) elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -188,7 +188,7 @@ class DynamicTableRegionMixin(BaseModel): this being a subclass of ``VectorData`` """ if self._index: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): # index returns an array of indices, # and indexing table with an array returns a list of rows return self.table[self._index[item]] @@ -200,7 +200,7 @@ class DynamicTableRegionMixin(BaseModel): else: raise ValueError(f"Dont know how to index with {item}, need an int or a slice") else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self.table[self.value[item]] elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -406,6 +406,32 @@ class DynamicTableMixin(BaseModel): model["colnames"].extend(colnames) return model + @model_validator(mode="after") + def cast_extra_columns(self): + """ + If extra columns are passed as just lists or arrays, cast to VectorData + before we resolve targets for VectorData and VectorIndex pairs. + + See :meth:`.cast_specified_columns` for handling columns in the class specification + """ + # if columns are not in the specification, cast to a generic VectorData + for key, val in self.__pydantic_extra__.items(): + if not isinstance(val, (VectorData, VectorIndex)): + try: + if key.endswith("_index"): + self.__pydantic_extra__[key] = VectorIndex( + name=key, description="", value=val + ) + else: + self.__pydantic_extra__[key] = VectorData( + name=key, description="", value=val + ) + except ValidationError as e: + raise ValidationError( + f"field {key} cannot be cast to VectorData from {val}" + ) from e + return self + @model_validator(mode="after") def resolve_targets(self) -> "DynamicTableMixin": """ @@ -442,11 +468,15 @@ class DynamicTableMixin(BaseModel): @field_validator("*", mode="wrap") @classmethod - def cast_columns( + def cast_specified_columns( cls, val: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo ) -> Any: """ - If columns are supplied as arrays, try casting them to the type before validating + If columns *in* the model specification are supplied as arrays, + try casting them to the type before validating. + + Columns that are not in the spec are handled separately in + :meth:`.cast_extra_columns` """ try: return handler(val) diff --git a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_7_0/hdmf_common_table.py b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_7_0/hdmf_common_table.py index 1d6e89f..3ffb25d 100644 --- a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_7_0/hdmf_common_table.py +++ b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_7_0/hdmf_common_table.py @@ -137,7 +137,7 @@ class VectorIndexMixin(BaseModel): if self.target is None: return self.value[item] else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self._getitem_helper(item) elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -188,7 +188,7 @@ class DynamicTableRegionMixin(BaseModel): this being a subclass of ``VectorData`` """ if self._index: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): # index returns an array of indices, # and indexing table with an array returns a list of rows return self.table[self._index[item]] @@ -200,7 +200,7 @@ class DynamicTableRegionMixin(BaseModel): else: raise ValueError(f"Dont know how to index with {item}, need an int or a slice") else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self.table[self.value[item]] elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -406,6 +406,32 @@ class DynamicTableMixin(BaseModel): model["colnames"].extend(colnames) return model + @model_validator(mode="after") + def cast_extra_columns(self): + """ + If extra columns are passed as just lists or arrays, cast to VectorData + before we resolve targets for VectorData and VectorIndex pairs. + + See :meth:`.cast_specified_columns` for handling columns in the class specification + """ + # if columns are not in the specification, cast to a generic VectorData + for key, val in self.__pydantic_extra__.items(): + if not isinstance(val, (VectorData, VectorIndex)): + try: + if key.endswith("_index"): + self.__pydantic_extra__[key] = VectorIndex( + name=key, description="", value=val + ) + else: + self.__pydantic_extra__[key] = VectorData( + name=key, description="", value=val + ) + except ValidationError as e: + raise ValidationError( + f"field {key} cannot be cast to VectorData from {val}" + ) from e + return self + @model_validator(mode="after") def resolve_targets(self) -> "DynamicTableMixin": """ @@ -442,11 +468,15 @@ class DynamicTableMixin(BaseModel): @field_validator("*", mode="wrap") @classmethod - def cast_columns( + def cast_specified_columns( cls, val: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo ) -> Any: """ - If columns are supplied as arrays, try casting them to the type before validating + If columns *in* the model specification are supplied as arrays, + try casting them to the type before validating. + + Columns that are not in the spec are handled separately in + :meth:`.cast_extra_columns` """ try: return handler(val) diff --git a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_8_0/hdmf_common_table.py b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_8_0/hdmf_common_table.py index 2c1798b..de43571 100644 --- a/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_8_0/hdmf_common_table.py +++ b/nwb_linkml/src/nwb_linkml/models/pydantic/hdmf_common/v1_8_0/hdmf_common_table.py @@ -137,7 +137,7 @@ class VectorIndexMixin(BaseModel): if self.target is None: return self.value[item] else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self._getitem_helper(item) elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -188,7 +188,7 @@ class DynamicTableRegionMixin(BaseModel): this being a subclass of ``VectorData`` """ if self._index: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): # index returns an array of indices, # and indexing table with an array returns a list of rows return self.table[self._index[item]] @@ -200,7 +200,7 @@ class DynamicTableRegionMixin(BaseModel): else: raise ValueError(f"Dont know how to index with {item}, need an int or a slice") else: - if isinstance(item, int): + if isinstance(item, (int, np.integer)): return self.table[self.value[item]] elif isinstance(item, (slice, Iterable)): if isinstance(item, slice): @@ -406,6 +406,32 @@ class DynamicTableMixin(BaseModel): model["colnames"].extend(colnames) return model + @model_validator(mode="after") + def cast_extra_columns(self): + """ + If extra columns are passed as just lists or arrays, cast to VectorData + before we resolve targets for VectorData and VectorIndex pairs. + + See :meth:`.cast_specified_columns` for handling columns in the class specification + """ + # if columns are not in the specification, cast to a generic VectorData + for key, val in self.__pydantic_extra__.items(): + if not isinstance(val, (VectorData, VectorIndex)): + try: + if key.endswith("_index"): + self.__pydantic_extra__[key] = VectorIndex( + name=key, description="", value=val + ) + else: + self.__pydantic_extra__[key] = VectorData( + name=key, description="", value=val + ) + except ValidationError as e: + raise ValidationError( + f"field {key} cannot be cast to VectorData from {val}" + ) from e + return self + @model_validator(mode="after") def resolve_targets(self) -> "DynamicTableMixin": """ @@ -442,11 +468,15 @@ class DynamicTableMixin(BaseModel): @field_validator("*", mode="wrap") @classmethod - def cast_columns( + def cast_specified_columns( cls, val: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo ) -> Any: """ - If columns are supplied as arrays, try casting them to the type before validating + If columns *in* the model specification are supplied as arrays, + try casting them to the type before validating. + + Columns that are not in the spec are handled separately in + :meth:`.cast_extra_columns` """ try: return handler(val) diff --git a/nwb_linkml/tests/test_includes/test_hdmf.py b/nwb_linkml/tests/test_includes/test_hdmf.py index 2d07d2d..e00c02e 100644 --- a/nwb_linkml/tests/test_includes/test_hdmf.py +++ b/nwb_linkml/tests/test_includes/test_hdmf.py @@ -211,7 +211,7 @@ def test_dynamictable_region_ragged(): name="table", description="a table what else would it be", id=np.arange(len(spike_idx)), - timeseries=spike_times, + timeseries=spike_times_flat, timeseries_index=spike_idx, ) region = DynamicTableRegion( @@ -242,3 +242,10 @@ def test_dynamictable_append_column(): def test_dynamictable_append_row(): pass + + +def test_dynamictable_extra_coercion(): + """ + Extra fields should be coerced to VectorData and have their + indexing relationships handled when passed as plain arrays. + """