Skip to content

Commit 6234ad6

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 217171a commit 6234ad6

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
@@ -25,7 +25,7 @@
2525
from .heading import Heading
2626
from .settings import config
2727
from .staged_insert import staged_insert1 as _staged_insert1
28-
from .utils import get_master, is_camel_case, user_choice
28+
from .utils import get_master, is_camel_case, is_valid_class_name, user_choice
2929

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

@@ -134,11 +134,19 @@ def declare(self, context=None):
134134
"""
135135
if self.connection.in_transaction:
136136
raise DataJointError("Cannot declare new tables inside a transaction, e.g. from inside a populate/make call")
137-
# Enforce strict CamelCase #1150
138-
if not is_camel_case(self.class_name):
137+
# Validate class name #1150
138+
if not is_valid_class_name(self.class_name):
139139
raise DataJointError(
140-
"Table class name `{name}` is invalid. Please use CamelCase. ".format(name=self.class_name)
141-
+ "Classes defining tables should be formatted in strict CamelCase."
140+
f"Table class name `{self.class_name}` is invalid. "
141+
"Class names must start with a capital letter and contain only "
142+
"alphanumeric characters (underscores allowed for legacy support)."
143+
)
144+
if not is_camel_case(self.class_name):
145+
warnings.warn(
146+
f"Table class name `{self.class_name}` contains underscores. "
147+
"CamelCase names without underscores are recommended.",
148+
UserWarning,
149+
stacklevel=2,
142150
)
143151
sql, _external_stores, primary_key, fk_attribute_map, pre_ddl, post_ddl = declare(
144152
self.full_table_name, self.definition, context, self.connection.adapter

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
@@ -66,6 +67,23 @@ def is_camel_case(s):
6667
return bool(re.match(r"^[A-Z][A-Za-z0-9]*$", s))
6768

6869

70+
def is_valid_class_name(s):
71+
"""
72+
Check if a string is a valid table class name.
73+
74+
Allows CamelCase with optional underscores for backwards compatibility.
75+
Strict CamelCase (without underscores) is preferred.
76+
77+
:param s: string to check
78+
:returns: True if the string is a valid class name, False otherwise
79+
Example:
80+
>>> is_valid_class_name("TableName") # returns True
81+
>>> is_valid_class_name("Table_Name") # returns True (legacy support)
82+
>>> is_valid_class_name("table_name") # returns False
83+
"""
84+
return bool(re.match(r"^[A-Z][A-Za-z0-9_]*$", s))
85+
86+
6987
def to_camel_case(s):
7088
"""
7189
Convert names with under score (_) separation into camel case names.
@@ -95,8 +113,19 @@ def from_camel_case(s):
95113
def convert(match):
96114
return ("_" if match.groups()[0] else "") + match.group(0).lower()
97115

116+
if not is_valid_class_name(s):
117+
raise DataJointError(
118+
"ClassName must be alphanumeric in CamelCase, begin with a capital letter"
119+
)
98120
if not is_camel_case(s):
99-
raise DataJointError("ClassName must be alphanumeric in CamelCase, begin with a capital letter")
121+
warnings.warn(
122+
f"Table class name `{s}` contains underscores. "
123+
"CamelCase names without underscores are recommended.",
124+
UserWarning,
125+
stacklevel=3,
126+
)
127+
# Remove underscores before conversion to avoid double underscores
128+
s = s.replace("_", "")
100129
return re.sub(r"(\B[A-Z])|(\b[A-Z])", convert, s)
101130

102131

tests/integration/test_declare.py

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

352352
def test_table_name_with_underscores(schema_any):
353353
"""
354-
Test issue #1150 -- Reject table names containing underscores. Tables should be in strict
355-
CamelCase.
354+
Test issue #1150 -- Table names with underscores should produce a warning but still work.
355+
Strict CamelCase is recommended.
356356
"""
357357

358358
class TableNoUnderscores(dj.Manual):
@@ -366,8 +366,11 @@ class Table_With_Underscores(dj.Manual):
366366
"""
367367

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

372375

373376
class TestSingletonTables:

0 commit comments

Comments
 (0)