Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

_gather_columns agnostic search #991

Merged
merged 24 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# HDMF Changelog

## HDMF 3.11.1 (Upcoming)

### Minor Improvements
- Updated `__gather_columns` to ignore the order of bases when generating columns from the super class. @mavaylon1 [#991](https://github.com/hdmf-dev/hdmf/pull/991)

## HDMF 3.11.0 (October 30, 2023)

### Enhancements
Expand Down
16 changes: 11 additions & 5 deletions src/hdmf/common/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,17 @@ def __gather_columns(cls, name, bases, classdict):
msg = "'__columns__' must be of type tuple, found %s" % type(cls.__columns__)
raise TypeError(msg)

if (len(bases) and 'DynamicTable' in globals() and issubclass(bases[-1], Container)
and bases[-1].__columns__ is not cls.__columns__):
new_columns = list(cls.__columns__)
new_columns[0:0] = bases[-1].__columns__ # prepend superclass columns to new_columns
cls.__columns__ = tuple(new_columns)
if len(bases) and 'DynamicTable' in globals():
for item in bases[::-1]: # look for __columns__ in the base classes, closest first
if issubclass(item, Container):
try:
if item.__columns__ is not cls.__columns__:
new_columns = list(cls.__columns__)
new_columns[0:0] = item.__columns__ # prepend superclass columns to new_columns
cls.__columns__ = tuple(new_columns)
break
except AttributeError: # raises error when "__columns__" is not an attr of item
continue

@docval({'name': 'name', 'type': str, 'doc': 'the name of this table'}, # noqa: C901
{'name': 'description', 'type': str, 'doc': 'a description of what is in this table'},
Expand Down
4 changes: 4 additions & 0 deletions src/hdmf/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,10 @@ class ExtenderMeta(ABCMeta):

@classmethod
def pre_init(cls, func):
"""
A decorator that sets a '__preinit' attribute on the target function and
then returns the function as a classmethod.
"""
setattr(func, cls.__preinit, True)
return classmethod(func)

Expand Down
19 changes: 18 additions & 1 deletion tests/unit/common/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
from hdmf.utils import StrDataset
from hdmf.data_utils import DataChunkIterator

from tests.unit.helpers.utils import get_temp_filepath
from tests.unit.helpers.utils import (get_temp_filepath, FooExtendDynamicTable0,
FooExtendDynamicTable1, FooExtendDynamicTable2)

try:
import linkml_runtime # noqa: F401
Expand Down Expand Up @@ -2676,3 +2677,19 @@ def test_list_prev_data_inc_precision_2steps(self):
index.add_vector(list(range(65536 - 255)))
self.assertEqual(index.data[0], 255) # make sure the 255 is upgraded
self.assertEqual(type(index.data[0]), np.uint32)


class TestDynamicTableSubclassColumns(TestCase):
def setUp(self):
self.foo1 = FooExtendDynamicTable0()
self.foo2 = FooExtendDynamicTable1()
self.foo3 = FooExtendDynamicTable2()

def test_columns(self):
self.assertEqual(self.foo1.__columns__,
({'name': 'col1', 'description': '...'}, {'name': 'col2', 'description': '...'}))
self.assertEqual(self.foo2.__columns__,
({'name': 'col1', 'description': '...'}, {'name': 'col2', 'description': '...'},
{'name': 'col3', 'description': '...'}, {'name': 'col4', 'description': '...'})
)
self.assertEqual(self.foo2.__columns__, self.foo3.__columns__)
50 changes: 49 additions & 1 deletion tests/unit/helpers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from copy import copy, deepcopy

from hdmf.build import BuildManager, ObjectMapper, TypeMap
from hdmf.container import Container, HERDManager, Data
from hdmf.common.table import DynamicTable
from hdmf.container import Container, HERDManager, Data, MultiContainerInterface
from hdmf.spec import (
AttributeSpec,
DatasetSpec,
Expand Down Expand Up @@ -653,3 +654,50 @@ class CustomSpecNamespace(SpecNamespace):
@classmethod
def types_key(cls):
return cls.__types_key

class FooExtendDynamicTable0(DynamicTable):
"""
Within PyNWB, PlaneSegmentation extends DynamicTable and sets __columns__. This class is a helper
class for testing and is directly meant to test __gather_columns, i.e., class generation, downstream.
"""
__columns__ = (
{'name': 'col1', 'description': '...'},
{'name': 'col2', 'description': '...'},
)

def __init__(self, **kwargs):
kwargs['name'] = 'foo0'
kwargs['description'] = '...'
super().__init__(**kwargs)


class FooExtendDynamicTable1(FooExtendDynamicTable0):
"""
In extensions, users can create new classes that inherit from classes that inherit from DynamicTable.
This is a helper class for testing and is directly meant to test __gather_columns, i.e.,
class generation, downstream.
"""
__columns__ = (
{'name': 'col3', 'description': '...'},
{'name': 'col4', 'description': '...'},
)

def __init__(self, **kwargs):
kwargs['name'] = 'foo1'
kwargs['description'] = '...'
super().__init__(**kwargs)


class FooExtendDynamicTable2(FooExtendDynamicTable1, MultiContainerInterface):
__clsconf__ = {
'add': '...',
'get': '...',
'create': '...',
'type': Container,
'attr': '...'
}

def __init__(self, **kwargs):
kwargs['name'] = 'foo2'
kwargs['description'] = '...'
super().__init__(**kwargs)