Skip to content

Commit 1fa3089

Browse files
fix: Allow table class names with underscores (with warning)
Changes strict CamelCase enforcement from error to warning for table class names containing underscores. This restores compatibility with legacy schemas that use names like MouseScoreSheet_BodyCondition. - Add is_valid_class_name() that allows underscores - Convert error to warning in table._declare() - Convert error to warning in from_camel_case() - Strip underscores before conversion to avoid double underscores - Update test to expect warning instead of error Fixes compatibility for users migrating from DataJoint <= 0.14.1. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d4faadc commit 1fa3089

File tree

3 files changed

+49
-9
lines changed

3 files changed

+49
-9
lines changed

src/datajoint/table.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from .heading import Heading
2727
from .settings import config
2828
from .staged_insert import staged_insert1 as _staged_insert1
29-
from .utils import get_master, is_camel_case, user_choice
29+
from .utils import get_master, is_camel_case, is_valid_class_name, user_choice
3030

3131
logger = logging.getLogger(__name__.split(".")[0])
3232

@@ -151,11 +151,19 @@ def declare(self, context=None):
151151
"""
152152
if self.connection.in_transaction:
153153
raise DataJointError("Cannot declare new tables inside a transaction, e.g. from inside a populate/make call")
154-
# Enforce strict CamelCase #1150
155-
if not is_camel_case(self.class_name):
154+
# Validate class name #1150
155+
if not is_valid_class_name(self.class_name):
156156
raise DataJointError(
157-
"Table class name `{name}` is invalid. Please use CamelCase. ".format(name=self.class_name)
158-
+ "Classes defining tables should be formatted in strict CamelCase."
157+
f"Table class name `{self.class_name}` is invalid. "
158+
"Class names must start with a capital letter and contain only "
159+
"alphanumeric characters (underscores allowed for legacy support)."
160+
)
161+
if not is_camel_case(self.class_name):
162+
warnings.warn(
163+
f"Table class name `{self.class_name}` contains underscores. "
164+
"CamelCase names without underscores are recommended.",
165+
UserWarning,
166+
stacklevel=2,
159167
)
160168
sql, _external_stores, primary_key, fk_attribute_map = declare(self.full_table_name, self.definition, context)
161169

src/datajoint/utils.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import re
44
import shutil
5+
import warnings
56
from pathlib import Path
67

78
from .errors import DataJointError
@@ -57,6 +58,23 @@ def is_camel_case(s):
5758
return bool(re.match(r"^[A-Z][A-Za-z0-9]*$", s))
5859

5960

61+
def is_valid_class_name(s):
62+
"""
63+
Check if a string is a valid table class name.
64+
65+
Allows CamelCase with optional underscores for backwards compatibility.
66+
Strict CamelCase (without underscores) is preferred.
67+
68+
:param s: string to check
69+
:returns: True if the string is a valid class name, False otherwise
70+
Example:
71+
>>> is_valid_class_name("TableName") # returns True
72+
>>> is_valid_class_name("Table_Name") # returns True (legacy support)
73+
>>> is_valid_class_name("table_name") # returns False
74+
"""
75+
return bool(re.match(r"^[A-Z][A-Za-z0-9_]*$", s))
76+
77+
6078
def to_camel_case(s):
6179
"""
6280
Convert names with under score (_) separation into camel case names.
@@ -86,8 +104,19 @@ def from_camel_case(s):
86104
def convert(match):
87105
return ("_" if match.groups()[0] else "") + match.group(0).lower()
88106

107+
if not is_valid_class_name(s):
108+
raise DataJointError(
109+
"ClassName must be alphanumeric in CamelCase, begin with a capital letter"
110+
)
89111
if not is_camel_case(s):
90-
raise DataJointError("ClassName must be alphanumeric in CamelCase, begin with a capital letter")
112+
warnings.warn(
113+
f"Table class name `{s}` contains underscores. "
114+
"CamelCase names without underscores are recommended.",
115+
UserWarning,
116+
stacklevel=3,
117+
)
118+
# Remove underscores before conversion to avoid double underscores
119+
s = s.replace("_", "")
91120
return re.sub(r"(\B[A-Z])|(\b[A-Z])", convert, s)
92121

93122

tests/integration/test_declare.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,8 @@ class IndexAttribute(dj.Manual):
348348

349349
def test_table_name_with_underscores(schema_any):
350350
"""
351-
Test issue #1150 -- Reject table names containing underscores. Tables should be in strict
352-
CamelCase.
351+
Test issue #1150 -- Table names with underscores should produce a warning but still work.
352+
Strict CamelCase is recommended.
353353
"""
354354

355355
class TableNoUnderscores(dj.Manual):
@@ -363,5 +363,8 @@ class Table_With_Underscores(dj.Manual):
363363
"""
364364

365365
schema_any(TableNoUnderscores)
366-
with pytest.raises(dj.DataJointError, match="must be alphanumeric in CamelCase"):
366+
# Underscores now produce a warning instead of an error (legacy support)
367+
with pytest.warns(UserWarning, match="contains underscores"):
367368
schema_any(Table_With_Underscores)
369+
# Verify the table was created successfully
370+
assert Table_With_Underscores.is_declared

0 commit comments

Comments
 (0)