diff --git a/src/datajoint/table.py b/src/datajoint/table.py index 7543c385e..ac72f7b32 100644 --- a/src/datajoint/table.py +++ b/src/datajoint/table.py @@ -120,6 +120,30 @@ def table_name(self): def class_name(self): return self.__class__.__name__ + # Base tier class names that should not raise errors when heading is None + _base_tier_classes = frozenset({"Table", "UserTable", "Lookup", "Manual", "Imported", "Computed", "Part"}) + + @property + def heading(self): + """ + Return the table's heading, or raise a helpful error if not configured. + + Overrides QueryExpression.heading to provide a clear error message + when the table is not properly associated with an activated schema. + For base tier classes (Lookup, Manual, etc.), returns None to support + introspection (e.g., help()). + """ + if self._heading is None: + # Don't raise error for base tier classes - they're used for introspection + if self.__class__.__name__ in self._base_tier_classes: + return None + raise DataJointError( + f"Table `{self.__class__.__name__}` is not properly configured. " + "Ensure the schema is activated before using the table. " + "Example: schema.activate('database_name') or schema = dj.Schema('database_name')" + ) + return self._heading + @property def definition(self): raise NotImplementedError("Subclasses of Table must implement the `definition` property") diff --git a/tests/integration/test_schema.py b/tests/integration/test_schema.py index d463ccf45..8cf231bf5 100644 --- a/tests/integration/test_schema.py +++ b/tests/integration/test_schema.py @@ -110,6 +110,32 @@ class UndecoratedClass(dj.Manual): print(a.full_table_name) +def test_non_activated_schema_heading_error(): + """ + Tables from non-activated schemas should raise informative errors. + Regression test for issue #1039. + """ + # Create schema without activating (no database name) + schema = dj.Schema() + + @schema + class TableA(dj.Manual): + definition = """ + id : int + --- + value : float + """ + + # Accessing heading should raise a helpful error + instance = TableA() + with pytest.raises(dj.DataJointError, match="not properly configured"): + _ = instance.heading + + # Operations that use heading should also raise helpful errors + with pytest.raises(dj.DataJointError, match="not properly configured"): + _ = instance.primary_key # Uses heading.primary_key + + def test_reject_decorated_part(schema_any): """ Decorating a dj.Part table should raise an informative exception.