Skip to content

Commit 5c95a01

Browse files
authored
Merge pull request #1119 from VisLab/action_update
Corrected handling of duplicate definitions
2 parents 3131e2c + aab19bc commit 5c95a01

File tree

10 files changed

+155
-42
lines changed

10 files changed

+155
-42
lines changed

hed/errors/error_messages.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def val_error_hed_placeholder_out_of_context(tag):
9090
@hed_tag_error(ValidationErrors.CURLY_BRACE_UNSUPPORTED_HERE, has_sub_tag=False,
9191
actual_code=SidecarErrors.SIDECAR_BRACES_INVALID)
9292
def val_error_curly_brace_unsupported_here(tag, problem_tag):
93-
return (f"Curly braces are only permitted in sidecars, fully wrapping text in place of a tag. "
93+
return (f"Curly braces are only permitted in metadata (e.g., a BIDS sidecar or NWB MeaningsTable) and must contain a column name. "
9494
f"Invalid character '{problem_tag}' in tag '{tag}'")
9595

9696

@@ -184,7 +184,7 @@ def val_error_extra_column(column_name):
184184

185185
@hed_error(ValidationErrors.SIDECAR_AND_OTHER_COLUMNS)
186186
def val_error_sidecar_with_column(column_names):
187-
return f"You cannot use a sidecar and tag or prefix columns at the same time. " \
187+
return f"You cannot use a column name in curly braces and to designate a tag column. " \
188188
f"Found {column_names}."
189189

190190

@@ -193,7 +193,7 @@ def val_error_duplicate_column(column_number, column_name, list_name):
193193
if column_name:
194194
return f"Found column '{column_name}' at index {column_number} twice in {list_name}."
195195
else:
196-
return f"Found column number {column_number} twice in {list_name}. This may indicate a mistake."
196+
return f"Found column number {column_number} twice in {list_name}. This may indicate a mistake."
197197

198198

199199
@hed_error(ValidationErrors.DUPLICATE_COLUMN_BETWEEN_SOURCES)
@@ -223,13 +223,13 @@ def val_error_extra_slashes_spaces(tag, problem_tag):
223223

224224
@hed_error(ValidationErrors.SIDECAR_KEY_MISSING, default_severity=ErrorSeverity.WARNING)
225225
def val_error_sidecar_key_missing(invalid_keys, category_keys, column_name):
226-
return f"Category keys {invalid_keys} do not exist in sidecar for column '{column_name}'. Valid keys are: {category_keys}"
226+
return f"Category values {invalid_keys} do not exist in metadata (e.g., BIDS sidecar or NWB meanings) for column '{column_name}'. Valid keys are: {category_keys}"
227227

228228

229229
@hed_error(ValidationErrors.TSV_COLUMN_MISSING, actual_code=ValidationErrors.SIDECAR_KEY_MISSING,
230230
default_severity=ErrorSeverity.WARNING)
231231
def val_error_tsv_column_missing(invalid_keys):
232-
return f"{invalid_keys} used as column references in sidecar but are not columns in the tabular file"
232+
return f"{invalid_keys} used as column references in metadata (e.g., BIDS sidecar or NWB meanings) but are not columns in the tabular file"
233233

234234

235235
@hed_tag_error(ValidationErrors.HED_DEF_EXPAND_INVALID, actual_code=ValidationErrors.DEF_EXPAND_INVALID)
@@ -307,17 +307,17 @@ def sidecar_error_blank_hed_string():
307307

308308
@hed_error(SidecarErrors.WRONG_HED_DATA_TYPE)
309309
def sidecar_error_hed_data_type(expected_type, given_type):
310-
return f"Invalid HED string datatype sidecar. Should be '{expected_type}', but got '{given_type}'"
310+
return f"Invalid datatype in metadata (e.g., a BIDS sidecar or NWB meanings). Should be '{expected_type}', but got '{given_type}'"
311311

312312

313313
@hed_error(SidecarErrors.INVALID_POUND_SIGNS_VALUE, actual_code=ValidationErrors.PLACEHOLDER_INVALID)
314314
def sidecar_error_invalid_pound_sign_count(pound_sign_count):
315-
return f"There should be exactly one # character in a sidecar string. Found {pound_sign_count}"
315+
return f"There should be exactly one # character in a HED value string. Found {pound_sign_count}"
316316

317317

318318
@hed_error(SidecarErrors.INVALID_POUND_SIGNS_CATEGORY, actual_code=ValidationErrors.PLACEHOLDER_INVALID)
319319
def sidecar_error_too_many_pound_signs(pound_sign_count):
320-
return f"There should be no # characters in a category sidecar string. Found {pound_sign_count}"
320+
return f"There should be no # characters in an annotation for a category value. Found {pound_sign_count}"
321321

322322

323323
@hed_error(SidecarErrors.UNKNOWN_COLUMN_TYPE)
@@ -328,7 +328,7 @@ def sidecar_error_unknown_column(column_name):
328328

329329
@hed_error(SidecarErrors.SIDECAR_HED_USED, actual_code=ValidationErrors.SIDECAR_INVALID)
330330
def sidecar_hed_used():
331-
return "'HED' is a reserved name and cannot be used as a sidecar except in expected places."
331+
return "'HED' is a reserved name and cannot be used as a metadata column key except in expected places."
332332

333333

334334
@hed_error(SidecarErrors.SIDECAR_NA_USED, actual_code=ValidationErrors.SIDECAR_INVALID)

hed/errors/error_reporter.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
default_sort_list = [
4343
ErrorContext.CUSTOM_TITLE,
4444
ErrorContext.FILE_NAME,
45+
ErrorContext.TABLE_NAME,
4546
ErrorContext.SIDECAR_COLUMN_NAME,
4647
ErrorContext.SIDECAR_KEY_NAME,
4748
ErrorContext.ROW,
@@ -744,7 +745,8 @@ def _format_single_context_string(context_type, context, tab_count=0):
744745
"""
745746
tab_string = tab_count * '\t'
746747
error_types = {
747-
ErrorContext.FILE_NAME: f"\nErrors in file '{context}'",
748+
ErrorContext.FILE_NAME: f"\nErrors in file '{context}':",
749+
ErrorContext.TABLE_NAME: f"\nErrors in table '{context}':",
748750
ErrorContext.SIDECAR_COLUMN_NAME: f"Column '{context}':",
749751
ErrorContext.SIDECAR_KEY_NAME: f"Key: {context}",
750752
ErrorContext.ROW: f'Issues in row {context}:',

hed/errors/error_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class ErrorContext:
2121
SCHEMA_SECTION = 'ec_section'
2222
SCHEMA_TAG = 'ec_schema_tag'
2323
SCHEMA_ATTRIBUTE = 'ec_attribute'
24+
TABLE_NAME = 'ec_table_name'
2425

2526

2627
class ValidationErrors:

hed/models/definition_dict.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ def __init__(self, def_dicts=None, hed_schema=None):
2929
if def_dicts:
3030
self.add_definitions(def_dicts, hed_schema)
3131

32-
def add_definitions(self, def_dicts, hed_schema=None):
32+
def add_definitions(self, defs, hed_schema=None):
3333
""" Add definitions from dict(s) or strings(s) to this dict.
3434
3535
Parameters:
36-
def_dicts (list, DefinitionDict, dict, or str): DefinitionDict or list of DefinitionDicts/strings/dicts
36+
defs (list, DefinitionDict, dict, or str): DefinitionDict or list of DefinitionDicts/strings/dicts
3737
whose definitions should be added.
3838
hed_schema (HedSchema or None): Required if passing strings or lists of strings, unused otherwise.
3939
@@ -42,23 +42,25 @@ def add_definitions(self, def_dicts, hed_schema=None):
4242
Note - You can mix and match types, eg [DefinitionDict, str, list of str] would be valid input.
4343
4444
Raises:
45-
TypeError: Bad type passed as def_dicts.
45+
TypeError: Bad type passed as defs.
4646
"""
47-
if not isinstance(def_dicts, list):
48-
def_dicts = [def_dicts]
49-
for def_dict in def_dicts:
50-
if isinstance(def_dict, (DefinitionDict, dict)):
51-
self._add_definitions_from_dict(def_dict)
52-
elif isinstance(def_dict, str) and hed_schema:
53-
self.check_for_definitions(HedString(def_dict, hed_schema))
54-
elif isinstance(def_dict, list) and hed_schema:
55-
for definition in def_dict:
56-
self.check_for_definitions(HedString(definition, hed_schema))
47+
if not isinstance(defs, list):
48+
defs = [defs]
49+
for definition in defs:
50+
if isinstance(definition, (DefinitionDict, dict)):
51+
self._add_definitions_from_dict(definition)
52+
elif isinstance(definition, str) and hed_schema:
53+
self._issues += self.check_for_definitions(HedString(definition, hed_schema))
54+
elif isinstance(definition, list) and hed_schema:
55+
for def_item in definition:
56+
self._issues = self.check_for_definitions(HedString(def_item, hed_schema))
5757
else:
58-
raise TypeError(f"Invalid type '{type(def_dict)}' passed to DefinitionDict")
58+
raise TypeError(f"Invalid type '{type(defs)}' passed to DefinitionDict")
5959

6060
def _add_definition(self, def_tag, def_value):
61-
if def_tag in self.defs:
61+
if def_tag in self.defs and def_value == self.defs[def_tag]:
62+
return
63+
elif def_tag in self.defs:
6264
error_context = self.defs[def_tag].source_context
6365
self._issues += ErrorHandler.format_error_from_context(DefinitionErrors.DUPLICATE_DEFINITION,
6466
error_context=error_context, def_name=def_tag, actual_error=DefinitionErrors.DUPLICATE_DEFINITION)
@@ -69,8 +71,10 @@ def _add_definitions_from_dict(self, def_dict):
6971
""" Add the definitions found in the given definition dictionary to this mapper.
7072
7173
Parameters:
72-
def_dict (DefinitionDict or dict): DefDict whose definitions should be added.
74+
def_dict (DefinitionDict or dict): Dictionary-like whose definitions should be added.
7375
76+
Note:
77+
Expects DefinitionEntries in the same form as a DefinitionDict
7478
"""
7579
for def_tag, def_value in def_dict.items():
7680
self._add_definition(def_tag, def_value)

hed/models/definition_entry.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def __init__(self, name, contents, takes_value, source_context):
1717
takes_value (bool): If True, expects ONE tag to have a single # sign in it.
1818
source_context (list, None): List (stack) of dictionaries giving context for reporting errors.
1919
"""
20-
self.name = name
20+
self.name = name.casefold()
2121
if contents:
2222
contents = contents.copy()
2323
contents.sort()
@@ -66,3 +66,19 @@ def get_definition(self, replace_tag, placeholder_value=None, return_copy_of_tag
6666

6767
def __str__(self):
6868
return str(self.contents)
69+
70+
def __eq__(self, other):
71+
""" Check equality based on name, contents, and takes_value.
72+
73+
Parameters:
74+
other (DefinitionEntry): Another DefinitionEntry to compare with.
75+
76+
Returns:
77+
bool: True if name, contents, and takes_value are equal, False otherwise.
78+
"""
79+
if not isinstance(other, DefinitionEntry):
80+
return False
81+
82+
return (self.name == other.name and
83+
self.contents == other.contents and
84+
self.takes_value == other.takes_value)

pyproject.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ authors = [
1616
{ name = "Owen Winterberg" },
1717
{ name = "Kay Robbins", email = "[email protected]" },
1818
]
19-
license = "MIT"
19+
license = {text = "MIT"}
2020
keywords = [] # Add keywords here if any
2121
classifiers = [
2222
"Programming Language :: Python :: 3",
2323
"Operating System :: OS Independent",
24+
"Development Status :: 5 - Production/Stable",
25+
"Intended Audience :: Developers",
26+
"Intended Audience :: Science/Research",
2427
]
2528

2629
requires-python = ">=3.9"
@@ -43,8 +46,10 @@ dependencies = [
4346
]
4447

4548
[project.urls]
46-
"Homepage" = "https://github.com/hed-standard/hed-python/"
49+
"Homepage" = "https://www.hedtags.org/"
50+
"Repo" = "https://github.com/hed-standard/hed-python/"
4751
"Bug Tracker" = "https://github.com/hed-standard/hed-python/issues"
52+
"API docs" = "https://www.hedtags.org/hed-python/"
4853

4954
[project.optional-dependencies]
5055
# Define any optional dependencies here

tests/models/test_definition_dict.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,21 @@ def test_altering_definition_contents(self):
147147

148148
self.assertNotEqual(hed_string1, hed_string2)
149149

150+
def test_add_definition(self):
151+
# Bad input string
152+
def_dict = DefinitionDict()
153+
def_dict.add_definitions("(Definition/testdefplaceholder,(Acceleration/#,Item/TestDef2,Red))",
154+
self.hed_schema)
155+
self.assertEqual(len(def_dict.issues), 1)
156+
self.assertEqual(len(def_dict.defs), 0)
157+
158+
# Good input string
159+
def_dict2 = DefinitionDict()
160+
def_dict2.add_definitions("(Definition/testdefplaceholder/#,(Acceleration/#,Item/TestDef2, Red))",
161+
self.hed_schema)
162+
self.assertEqual(len(def_dict2.issues), 0)
163+
self.assertEqual(len(def_dict2.defs), 1)
164+
150165

151166
if __name__ == '__main__':
152167
unittest.main()

tests/models/test_definition_entry.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,77 @@
11
import unittest
2+
import os
3+
from hed.schema import load_schema_version
4+
from hed.models.definition_entry import DefinitionEntry
5+
from hed.models.hed_string import HedString
26

37

48
class Test(unittest.TestCase):
59

610
@classmethod
711
def setUpClass(cls):
8-
pass
12+
cls.base_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/')
13+
hed_xml_file = os.path.join(cls.base_data_dir, "schema_tests/HED8.0.0t.xml")
14+
hed_schema = load_schema_version("8.4.0")
15+
cls.contents1 = HedString("(Sensory-event)", hed_schema)
16+
cls.contents2 = HedString("(Agent-action)", hed_schema)
17+
cls.contents3 = HedString("(Sensory-event)", hed_schema)
18+
cls.hed_schema = hed_schema
19+
20+
def test_definition_entry_init(self):
21+
"""Test basic initialization of DefinitionEntry."""
22+
name = "TestDef"
23+
takes_value = True
24+
source_context = [{"line": 1}]
25+
26+
entry = DefinitionEntry(name, self.contents1, True, source_context)
27+
self.assertEqual(entry.name, name.casefold())
28+
self.assertIsNotNone(entry.contents)
29+
self.assertEqual(entry.takes_value, takes_value)
30+
self.assertEqual(entry.source_context, source_context)
31+
self.assertEqual(str(entry), str(self.contents1))
32+
33+
def test_none_contents(self):
34+
"""Test with None contents."""
35+
entry = DefinitionEntry("TestDef", None, False, None)
36+
self.assertIsNone(entry.contents)
37+
self.assertFalse(entry.takes_value)
38+
self.assertEqual(str(entry), "None")
39+
40+
def test_eq_method(self):
41+
"""Test __eq__ method"""
42+
name = "TestDef"
43+
takes_value = True
44+
source_context1 = [{"line": 1}]
45+
source_context2 = [{"line": 2}] # Different source context
46+
47+
entry1 = DefinitionEntry(name, self.contents1, takes_value, source_context1)
48+
entry2 = DefinitionEntry(name, self.contents1, takes_value, source_context2)
49+
entry3 = DefinitionEntry(name, self.contents3, takes_value, source_context2)
50+
entry4 = DefinitionEntry("TESTDEF", self.contents3, takes_value, source_context1)
51+
# Should be equal despite different source contexts
52+
self.assertEqual(entry1, entry2)
53+
self.assertTrue(entry1 == entry2)
54+
# Should be equal as contents are the same
55+
self.assertEqual(entry1, entry3)
56+
self.assertTrue(entry1 == entry3)
57+
# Should be equal with different cased names
58+
self.assertEqual(entry1, entry4)
59+
self.assertTrue(entry1 == entry4)
60+
61+
# Not equal with different contents
62+
entry5 = DefinitionEntry(name, self.contents2, takes_value, None)
63+
self.assertNotEqual(entry1, entry5)
64+
self.assertFalse(entry1 == entry5)
65+
66+
# Not equal with different takes value
67+
entry6 = DefinitionEntry(name, self.contents2, True, None)
68+
self.assertNotEqual(entry1, entry6)
69+
self.assertFalse(entry1 == entry6)
70+
71+
# Not equal with different contents
72+
entry7 = DefinitionEntry(name, None, False, None)
73+
self.assertNotEqual(entry1, entry7)
74+
self.assertFalse(entry1 == entry7)
975

1076

1177
if __name__ == '__main__':

tests/models/test_sidecar.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from hed.errors import HedFileError, ValidationErrors
77
from hed.models import ColumnMetadata, HedString, Sidecar
88
from hed import schema
9-
from hed.models import DefinitionDict
9+
from hed.models import DefinitionDict, DefinitionEntry
1010
from hed.errors import ErrorHandler
1111

1212

@@ -112,11 +112,14 @@ def test_validate_column_group(self):
112112

113113
def test_duplicate_def(self):
114114
sidecar = self.json_def_sidecar
115-
115+
# If external defs are the same, no error
116116
duplicate_dict = sidecar.extract_definitions(hed_schema=self.hed_schema)
117117
issues = sidecar.validate(self.hed_schema, extra_def_dicts=duplicate_dict, error_handler=ErrorHandler(False))
118-
self.assertEqual(len(issues), 5)
119-
self.assertTrue(issues[0]['code'], ValidationErrors.DEFINITION_INVALID)
118+
self.assertEqual(len(issues), 0)
119+
test_dict = {"jsonfiledef3": DefinitionEntry("jsonfiledef3", None, False, None)}
120+
issues2 = sidecar.validate(self.hed_schema, extra_def_dicts=test_dict, error_handler=ErrorHandler(False))
121+
self.assertEqual(len(issues2), 1)
122+
self.assertTrue(issues2[0]['code'], ValidationErrors.DEFINITION_INVALID)
120123

121124
def test_save_load(self):
122125
sidecar = Sidecar(self.json_def_filename)

tests/validator/test_def_validator.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,15 @@ def test_duplicate_def(self):
8787
error_handler.push_error_context(ErrorContext.ROW, 5)
8888
def_dict.check_for_definitions(def_string, error_handler=error_handler)
8989
self.assertEqual(len(def_dict.issues), 0)
90-
91-
def_validator = DefValidator([def_dict, def_dict])
92-
self.assertEqual(len(def_validator.issues), 1)
93-
self.assertTrue('ec_row' in def_validator.issues[0])
94-
95-
def_dict = DefinitionDict([def_dict, def_dict, def_dict])
96-
self.assertEqual(len(def_dict.issues), 2)
97-
self.assertTrue('ec_row' in def_dict.issues[0])
90+
def_dict2 = DefinitionDict()
91+
def_dict2.add_definitions("(Definition/testdefplaceholder/#,(Acceleration/#,Item/TestDef2, Red))", self.hed_schema)
92+
self.assertEqual(len(def_dict2.issues), 0)
93+
def_dict3 = DefinitionDict([def_dict, def_dict2])
94+
self.assertEqual(len(def_dict3.issues), 1)
95+
96+
#
97+
# def_validator = DefValidator([def_dict, def_dict2])
98+
# self.assertEqual(len(def_validator.issues), 1)
9899

99100

100101
class TestDefErrors(unittest.TestCase):

0 commit comments

Comments
 (0)