diff --git a/src/datajoint/table.py b/src/datajoint/table.py index 16cb51b6d..950ab513f 100644 --- a/src/datajoint/table.py +++ b/src/datajoint/table.py @@ -151,11 +151,20 @@ def declare(self, context=None): """ if self.connection.in_transaction: raise DataJointError("Cannot declare new tables inside a transaction, e.g. from inside a populate/make call") - # Enforce strict CamelCase #1150 - if not is_camel_case(self.class_name): + # Validate class name #1150 + class_name = self.class_name + if "_" in class_name: + warnings.warn( + f"Table class name `{class_name}` contains underscores. " + "CamelCase names without underscores are recommended.", + UserWarning, + stacklevel=2, + ) + class_name = class_name.replace("_", "") + if not is_camel_case(class_name): raise DataJointError( - "Table class name `{name}` is invalid. Please use CamelCase. ".format(name=self.class_name) - + "Classes defining tables should be formatted in strict CamelCase." + f"Table class name `{self.class_name}` is invalid. " + "Class names must be in CamelCase, starting with a capital letter." ) sql, _external_stores, primary_key, fk_attribute_map = declare(self.full_table_name, self.definition, context) diff --git a/src/datajoint/user_tables.py b/src/datajoint/user_tables.py index 942179685..f85273a1e 100644 --- a/src/datajoint/user_tables.py +++ b/src/datajoint/user_tables.py @@ -32,6 +32,7 @@ "to_arrow", "to_arrays", "keys", + "fetch", "fetch1", "head", "tail", diff --git a/src/datajoint/utils.py b/src/datajoint/utils.py index e8303a993..d7bf9ac6d 100644 --- a/src/datajoint/utils.py +++ b/src/datajoint/utils.py @@ -2,6 +2,7 @@ import re import shutil +import warnings from pathlib import Path from .errors import DataJointError @@ -86,6 +87,14 @@ def from_camel_case(s): def convert(match): return ("_" if match.groups()[0] else "") + match.group(0).lower() + # Handle underscores: warn and remove them + if "_" in s: + warnings.warn( + f"Table class name `{s}` contains underscores. " "CamelCase names without underscores are recommended.", + UserWarning, + stacklevel=3, + ) + s = s.replace("_", "") if not is_camel_case(s): raise DataJointError("ClassName must be alphanumeric in CamelCase, begin with a capital letter") return re.sub(r"(\B[A-Z])|(\b[A-Z])", convert, s) diff --git a/tests/integration/test_declare.py b/tests/integration/test_declare.py index 3097a9457..d82f9e5cc 100644 --- a/tests/integration/test_declare.py +++ b/tests/integration/test_declare.py @@ -348,8 +348,8 @@ class IndexAttribute(dj.Manual): def test_table_name_with_underscores(schema_any): """ - Test issue #1150 -- Reject table names containing underscores. Tables should be in strict - CamelCase. + Test issue #1150 -- Table names with underscores should produce a warning but still work. + Strict CamelCase is recommended. """ class TableNoUnderscores(dj.Manual): @@ -363,5 +363,8 @@ class Table_With_Underscores(dj.Manual): """ schema_any(TableNoUnderscores) - with pytest.raises(dj.DataJointError, match="must be alphanumeric in CamelCase"): + # Underscores now produce a warning instead of an error (legacy support) + with pytest.warns(UserWarning, match="contains underscores"): schema_any(Table_With_Underscores) + # Verify the table was created successfully + assert Table_With_Underscores.is_declared diff --git a/tests/integration/test_utils.py b/tests/integration/test_utils.py index 2781554a1..49e735701 100644 --- a/tests/integration/test_utils.py +++ b/tests/integration/test_utils.py @@ -26,12 +26,14 @@ def test_from_camel_case(): assert from_camel_case("AllGroups") == "all_groups" with pytest.raises(DataJointError): from_camel_case("repNames") - with pytest.raises(DataJointError): - from_camel_case("10_all") + with pytest.warns(UserWarning, match="contains underscores"): + with pytest.raises(DataJointError): + from_camel_case("10_all") with pytest.raises(DataJointError): from_camel_case("hello world") - with pytest.raises(DataJointError): - from_camel_case("#baisc_names") + with pytest.warns(UserWarning, match="contains underscores"): + with pytest.raises(DataJointError): + from_camel_case("#baisc_names") def test_to_camel_case():