diff --git a/lamindb/models/feature.py b/lamindb/models/feature.py index ae0078b7d..e5c86d1e1 100644 --- a/lamindb/models/feature.py +++ b/lamindb/models/feature.py @@ -50,6 +50,7 @@ Registry, Space, SQLRecord, + UNSET, _get_record_kwargs, pop_space_branch_kwargs, ) @@ -670,7 +671,7 @@ def process_init_feature_param(args, kwargs): name: str = kwargs.pop("name", None) dtype: SimpleDtype | SimpleDtypeStr | str | None = kwargs.pop("dtype", None) is_type: bool = kwargs.pop("is_type", False) - type_: Feature | str | None = kwargs.pop("type", None) + type_: Feature | str | None = kwargs.pop("type", UNSET) description: str | None = kwargs.pop("description", None) space_branch_kwargs = pop_space_branch_kwargs(kwargs) _skip_validation = kwargs.pop("_skip_validation", False) diff --git a/lamindb/models/record.py b/lamindb/models/record.py index 93213b083..13d50722a 100644 --- a/lamindb/models/record.py +++ b/lamindb/models/record.py @@ -38,6 +38,7 @@ IsLink, Space, SQLRecord, + UNSET, _get_record_kwargs, pop_space_branch_kwargs, ) @@ -900,7 +901,7 @@ def __init__( if len(args) > 0: raise ValueError("Only one non-keyword arg allowed") name: str = kwargs.pop("name", None) - type: str | None = kwargs.pop("type", None) + type: str | None = kwargs.pop("type", UNSET) is_type: bool = kwargs.pop("is_type", False) features: dict[str | Feature, Any] | None = kwargs.pop("features", None) description: str | None = kwargs.pop("description", None) diff --git a/lamindb/models/schema.py b/lamindb/models/schema.py index c92a49554..42225807c 100644 --- a/lamindb/models/schema.py +++ b/lamindb/models/schema.py @@ -48,6 +48,7 @@ Registry, Space, SQLRecord, + UNSET, _get_record_kwargs, init_self_from_db, pop_space_branch_kwargs, @@ -672,7 +673,7 @@ def __init__( description: str | None = kwargs.pop("description", None) itype: str | SQLRecord | DeferredAttribute | None = kwargs.pop("itype", None) flexible: bool | None = kwargs.pop("flexible", None) - type: Feature | None = kwargs.pop("type", None) + type: Feature | None = kwargs.pop("type", UNSET) is_type: bool = kwargs.pop("is_type", False) otype: str | None = kwargs.pop("otype", None) suffix: str | None = kwargs.pop("suffix", None) @@ -857,7 +858,7 @@ def _validate_kwargs_calculate_hash( validated_kwargs = { "name": name, "description": description, - "type": type, + "type": None if type is UNSET else type, "is_type": is_type, "_dtype_str": dtype, "otype": otype, diff --git a/lamindb/models/sqlrecord.py b/lamindb/models/sqlrecord.py index 0255522a3..e7be87647 100644 --- a/lamindb/models/sqlrecord.py +++ b/lamindb/models/sqlrecord.py @@ -107,6 +107,10 @@ T = TypeVar("T", bound="SQLRecord") + +# Sentinel to distinguish "user didn't pass type=" from "user explicitly passed type=None". +# Uses object() so identity checks (is) never call __eq__ on model instances. +UNSET = object() IPYTHON = getattr(builtins, "__IPYTHON__", False) UNIQUE_FIELD_NAMES = { "root", @@ -1158,6 +1162,10 @@ class Meta: def __init__(self, *args, **kwargs): skip_validation = kwargs.pop("_skip_validation", False) + # strip sentinel before validate_fields and Django's Model.__init__ see it + # `is` never calls __eq__, so FeaturePredicate objects are safe + if isinstance(self, HasType) and kwargs.get("type", UNSET) is UNSET: + kwargs.pop("type", None) if not args: def resolve_fk_or_id(field_name: str) -> bool: diff --git a/lamindb/models/ulabel.py b/lamindb/models/ulabel.py index be30e2698..4582a3c8a 100644 --- a/lamindb/models/ulabel.py +++ b/lamindb/models/ulabel.py @@ -25,6 +25,7 @@ HasType, IsLink, SQLRecord, + UNSET, _get_record_kwargs, pop_space_branch_kwargs, ) @@ -249,7 +250,7 @@ def __init__( if len(args) > 0: raise ValueError("Only one non-keyword arg allowed") name: str = kwargs.pop("name", None) - type: str | None = kwargs.pop("type", None) + type: str | None = kwargs.pop("type", UNSET) is_type: bool = kwargs.pop("is_type", False) description: str | None = kwargs.pop("description", None) reference: str | None = kwargs.pop("reference", None)