From ab4a4ce8ec9c7499edadde1858867fcba8374f11 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Wed, 30 Sep 2026 17:13:28 -0500 Subject: [PATCH] feat: dj.Entry, dj.Ingest and dj.Compute as tier names on one axis The populated tiers sit on inconsistent axes. `Manual` names the writer but means the origin; `Imported` names the origin but means the writer; `Computed` names the result. A reader reasoning from the names alone lands in the wrong tier, and the recognizable outcome is an `Imported` table with no make() and a permanent allow_direct_insert=True papering over a modeling error. The new names put all of them on one question -- what puts rows in this table -- and the grammar carries it: nouns are tables something else fills, verbs are precisely the two things populate() does. Lookup noun the code, via contents Entry noun a writer outside the table Ingest verb the table's own make(), reading an external source Compute verb the table's own make(), deriving from other tables These are assignments, not subclasses, so a table declared either way is the same class: same SQL prefix, same Role, same tier detection, same DDL. That is not a property to be maintained but one that cannot be broken. This is the 2.3.4 half of #1546: the names become available and nothing else changes. Defaults, repr, diagram labels and what the documentation teaches stay on the old names until 2.4. `Manual`, `Imported` and `Computed` are permanent aliases -- no deprecation, no migration, no broken tutorials or third-party code, ever. --- src/datajoint/__init__.py | 5 ++- src/datajoint/user_tables.py | 13 ++++++ tests/unit/test_tier_aliases.py | 80 +++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_tier_aliases.py diff --git a/src/datajoint/__init__.py b/src/datajoint/__init__.py index 9728d2781..69c6df269 100644 --- a/src/datajoint/__init__.py +++ b/src/datajoint/__init__.py @@ -33,9 +33,12 @@ "AutoPopulate", "Job", "Manual", + "Entry", "Lookup", "Imported", + "Ingest", "Computed", + "Compute", "Part", "Not", "AndList", @@ -95,7 +98,7 @@ from .autopopulate import AutoPopulate from .jobs import Job from .table import FreeTable as _FreeTable, Table, ValidationResult -from .user_tables import Computed, Imported, Lookup, Manual, Part +from .user_tables import Compute, Computed, Entry, Imported, Ingest, Lookup, Manual, Part from .version import __version__ # ============================================================================= diff --git a/src/datajoint/user_tables.py b/src/datajoint/user_tables.py index a1ffd274e..938ac4352 100644 --- a/src/datajoint/user_tables.py +++ b/src/datajoint/user_tables.py @@ -262,6 +262,19 @@ def alter(self, prompt=True, context=None): super().alter(prompt=prompt, context=context or self.declaration_context) +#: Tier names on a single axis -- what puts rows in the table. ``Manual`` names +#: its writer but means the origin; ``Imported`` names the origin but means the +#: writer. ``Entry``, ``Ingest`` and ``Compute`` put all of them on the one +#: question, and the grammar carries it: nouns are tables something else fills, +#: verbs are the two things ``populate()`` does. +#: +#: These are the same classes, not subclasses, so a table declared either way is +#: identical -- same SQL prefix, same ``Role``, same tier detection. The old +#: names are permanent aliases, never deprecated. See #1546. +Entry = Manual +Ingest = Imported +Compute = Computed + user_table_classes = (Manual, Lookup, Computed, Imported, Part) diff --git a/tests/unit/test_tier_aliases.py b/tests/unit/test_tier_aliases.py new file mode 100644 index 000000000..0fb1b7dc4 --- /dev/null +++ b/tests/unit/test_tier_aliases.py @@ -0,0 +1,80 @@ +"""`Entry`/`Ingest`/`Compute` are the old tiers under names on one axis. + +#1546: `Manual` names its writer but means the origin; `Imported` names the +origin but means the writer. The new names put all of them on one question -- +what puts rows in this table -- and the old names stay permanently. + +These are aliases, not subclasses, so "identical table" is not a property to be +maintained but one that cannot be broken. These tests pin that, plus the +transparency checks the issue asks for: SQL prefix, Role, tier detection, and +`lookup_class_name`. +""" + +import pytest + +import datajoint as dj +from datajoint.settings import Role, role_to_prefix +from datajoint.user_tables import _get_tier + +PAIRS = [ + ("Entry", "Manual", Role.manual), + ("Ingest", "Imported", Role.imported), + ("Compute", "Computed", Role.computed), +] + + +@pytest.mark.parametrize("new, old, _role", PAIRS) +def test_alias_is_the_same_class(new, old, _role): + """Not a subclass: the same object, so nothing can drift between them.""" + assert getattr(dj, new) is getattr(dj, old) + + +@pytest.mark.parametrize("new, old, role", PAIRS) +def test_prefix_matches_the_role(new, old, role): + assert getattr(dj, new)._prefix == role_to_prefix[role] + + +@pytest.mark.parametrize("new, old, _role", PAIRS) +def test_exported_from_the_package(new, old, _role): + assert new in dj.__all__ + assert old in dj.__all__ + + +def test_lookup_and_part_are_unchanged(): + """The issue renames three tiers; these two already sit on the axis.""" + assert not hasattr(dj, "LookupAlias") + assert dj.Lookup._prefix == "#" + assert dj.Part.__name__ == "Part" + + +@pytest.mark.parametrize( + "table_name, expected", + [ + ("subject", "Manual"), + ("#param", "Lookup"), + ("_ingest", "Imported"), + ("__analysis", "Computed"), + ("subject__detail", "Part"), + ], +) +def test_tier_detection_resolves_to_one_class(table_name, expected): + """Tier detection is by SQL prefix, so the alias cannot confuse it.""" + tier = _get_tier(f"`lab`.`{table_name}`") + assert tier is not None and tier.__name__ == expected + + +def test_declaring_either_way_produces_the_same_table_name(): + """`_prefix` plus the class name is the whole of the table name.""" + from datajoint.utils import from_camel_case + + class ViaOld(dj.Manual): + definition = "" + + class ViaNew(dj.Entry): + definition = "" + + assert ViaOld._prefix == ViaNew._prefix + assert from_camel_case("Subject") == "subject" + # Both inherit the identical metaclass machinery, so the computed name for a + # given class name is the same by construction. + assert type(ViaOld) is type(ViaNew)