diff --git a/cid/__init__.py b/cid/__init__.py index 178a710..5702af0 100644 --- a/cid/__init__.py +++ b/cid/__init__.py @@ -8,6 +8,7 @@ CIDJSONEncoder, CIDv0, CIDv1, + Undef, extract_encoding, from_bytes, from_bytes_strict, diff --git a/cid/cid.py b/cid/cid.py index 6f8ad8b..49b3f15 100644 --- a/cid/cid.py +++ b/cid/cid.py @@ -186,6 +186,39 @@ def prefix(self) -> "Prefix": ) +class _UndefCID: + """Sentinel for undefined/nil CID (go-cid ``Undef``).""" + + _instance: "_UndefCID | None" = None + + def __new__(cls) -> "_UndefCID": + if cls._instance is None: + cls._instance = cast("_UndefCID", super().__new__(cls)) + return cls._instance + + def defined(self) -> bool: + """Return False; an undefined CID is never defined.""" + return False + + def __bool__(self) -> bool: + return False + + def __repr__(self) -> str: + return "CID.Undef" + + def __str__(self) -> str: + return "CID.Undef" + + def __eq__(self, other: object) -> bool: + return isinstance(other, _UndefCID) + + def __hash__(self) -> int: + return hash(_UndefCID) + + +Undef = _UndefCID() + + class CIDv0(BaseCID): """CID version 0 object""" diff --git a/docs/usage.rst b/docs/usage.rst index 41f9a77..3b84969 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -352,7 +352,7 @@ Defined Check .. code-block:: python - >>> from cid import CIDv0 + >>> from cid import CIDv0, Undef >>> import multihash >>> import hashlib >>> @@ -363,6 +363,12 @@ Defined Check >>> cid = CIDv0(mhash) >>> cid.defined() True + >>> + >>> # Undef is a sentinel for an undefined/nil CID (like go-cid Undef) + >>> Undef.defined() + False + >>> bool(Undef) + False Stream Parsing -------------- diff --git a/newsfragments/68.feature.rst b/newsfragments/68.feature.rst new file mode 100644 index 0000000..6a630f2 --- /dev/null +++ b/newsfragments/68.feature.rst @@ -0,0 +1 @@ +Add ``Undef`` sentinel for representing undefined/nil CIDs, matching go-cid ``Undef``. diff --git a/tests/test_new_features.py b/tests/test_new_features.py index afb570d..a44a2bd 100644 --- a/tests/test_new_features.py +++ b/tests/test_new_features.py @@ -10,6 +10,7 @@ CIDSet, CIDv0, CIDv1, + Undef, V0Builder, V1Builder, extract_encoding, @@ -19,6 +20,7 @@ must_parse, parse_ipfs_path, ) +from cid.cid import _UndefCID @pytest.fixture @@ -321,6 +323,37 @@ def test_defined_cidv1(self, cidv1): assert cidv1.defined() is True +class TestUndef: + """Tests for Undef sentinel""" + + def test_undef_defined_is_false(self): + assert Undef.defined() is False + + def test_undef_bool_is_false(self): + assert bool(Undef) is False + + def test_undef_repr_and_str(self): + assert repr(Undef) == "CID.Undef" + assert str(Undef) == "CID.Undef" + + def test_undef_is_singleton(self): + assert Undef is Undef + assert _UndefCID() is Undef + + def test_undef_equality(self, cidv0): + assert Undef == Undef + assert Undef == _UndefCID() + assert Undef != cidv0 + + def test_undef_hashable(self): + assert Undef in {Undef} + mapping = {Undef: "unset"} + assert mapping[Undef] == "unset" + + def test_undef_not_cid_instance(self): + assert not isinstance(Undef, (CIDv0, CIDv1)) + + class TestFromReader: """Tests for from_reader function"""