diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e353870..8276cec 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,13 @@ Changelog Versions follow `Semantic Versioning `_ +`Unreleased`_ +------------- +Fixed +~~~~~ +* :meth:`.ULID.from_str` accepts lower case strings again, as the ULID spec defines the string + representation as case insensitive. Lower case input has been rejected since 2.6.0. + `4.0.1`_ - 2026-07-20 --------------------- Changed @@ -265,6 +272,7 @@ Changed * The package now has no external dependencies. * The test-coverage has been raised to 100%. +.. _Unreleased: https://github.com/mdomke/python-ulid/compare/4.0.1...HEAD .. _4.0.1: https://github.com/mdomke/python-ulid/compare/4.0.0...4.0.1 .. _4.0.0: https://github.com/mdomke/python-ulid/compare/3.2.1...4.0.0 .. _3.2.1: https://github.com/mdomke/python-ulid/compare/3.2.0...3.2.1 diff --git a/tests/test_ulid.py b/tests/test_ulid.py index 0cac156..3703cb4 100644 --- a/tests/test_ulid.py +++ b/tests/test_ulid.py @@ -177,6 +177,12 @@ def test_idempotency() -> None: assert ULID.parse(ulid.bytes) == ulid +def test_from_str_is_case_insensitive() -> None: + ulid = ULID() + assert ULID.from_str(str(ulid).lower()) == ulid + assert ULID.parse(str(ulid).lower()) == ulid + + def test_to_uuid4() -> None: ulid = ULID() uuid = ulid.to_uuid4() @@ -350,6 +356,7 @@ def test_ulid_from_timestamp_overflow() -> None: (ULID.from_str, "not-enough"), # invalid length (ULID.from_str, 123), # inavlid type (ULID.from_str, "notavalidulidnotavalidulid"), # invalid alphabet + (ULID.from_str, "0" * 25 + "u"), # excluded letter in lower case (ULID.from_str, "Z" * 26), # invalid timestamp (ULID.from_int, "not-an-int"), # invalid type (ULID.from_uuid, "not-a-uuid"), # invalid type diff --git a/ulid/base32.py b/ulid/base32.py index fb94031..2cbd532 100644 --- a/ulid/base32.py +++ b/ulid/base32.py @@ -7,6 +7,8 @@ # https://github.com/RobThree/NUlid/blob/89f5a9fc827d191ae5adafe42547575ed3a47723/NUlid/Ulid.cs#L168 ENCODE: str = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" +# The ULID spec defines the string representation as case insensitive. +DECODE_ALPHABET: str = ENCODE + ENCODE.lower() DECODE: Sequence[int] = [ 0xFF, 0xFF, @@ -194,7 +196,7 @@ def encode_randomness(binary: bytes) -> str: def decode(encoded: str) -> bytes: if len(encoded) != constants.REPR_LEN: raise ValueError("Encoded ULID has to be exactly 26 characters long.") - if any((c not in ENCODE) for c in encoded): + if any((c not in DECODE_ALPHABET) for c in encoded): raise ValueError(f"Encoded ULID can only consist of letters in {ENCODE}.") return decode_timestamp(encoded[: constants.TIMESTAMP_REPR_LEN]) + decode_randomness( encoded[constants.TIMESTAMP_REPR_LEN :]