Skip to content

Decouple generator state#65

Merged
mdomke merged 8 commits into
mainfrom
decouple-generator-state
Jul 20, 2026
Merged

Decouple generator state#65
mdomke merged 8 commits into
mainfrom
decouple-generator-state

Conversation

@mdomke

@mdomke mdomke commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

Replaces the internal ValueProvider with a public ULIDGenerator that owns ULID generation — sampling the clock, sourcing randomness, and enforcing a configurable monotonicity policy. ULID() and the ULID.from_* constructors delegate to a swappable module-level default_generator.

⚠️ Breaking change (4.0.0)

  • ValueProvider and the ULID.provider class attribute have been removed. To customize generation, build a ULIDGenerator and either call generate() or assign it to ulid.default_generator:
import ulid
from ulid import ULIDGenerator, LaxMonotonicPolicy

ulid.default_generator = ULIDGenerator(policy=LaxMonotonicPolicy())
  • ULID.from_uuidv7 renamed to ULID.from_uuid7, for symmetry with ULID.to_uuid7.
  • The internal validate_type decorator was also removed; the ULID.from_* constructors still raise TypeError for wrong-typed input, so runtime behavior is unchanged.

Highlights

  • New public ULIDGenerator — configurable clock, randomness source, and policy.
  • Pluggable policies: StrictMonotonicPolicy (default), LaxMonotonicPolicy, PureRandomPolicy, plus the MonotonicityPolicy protocol and BaseMonotonicPolicy base for custom policies.
  • Swappable module-level ulid.default_generator.
  • API reference + README "Generators and policies" section; 4.0.0 and 3.2.1 changelog entries.

mdomke added 8 commits July 20, 2026 11:30
…ULIDGenerator

Extracts the stateful monotonicity tracking, thread locks, and clock/randomness
sourcing out of the global class-level scope and encapsulates it in a clean,
isolated `ULIDGenerator` class.

Why this change was made:
1. Global isolation: Avoids global mutable state pollution where calling ULID()
   affects the generation of all other ULIDs in different parts of an application.
2. Testability: Tests no longer need to mutate global class-level properties of
   `ULID.provider` directly. They can now cleanly instantiate isolated generators
   and inject mock clocks or mock randomness sources, aligning with the principle
   "the interface is the test surface".
3. Customizability: Callers can now configure and instantiate distinct generator
   instances with different lifecycles and behaviors.

All backward compatibility is fully preserved. A default shared generator is
created inside `ulid` so that calls to `ULID()` and `from_timestamp` continue
working out-of-the-box. Added full unit tests to cover state isolation and
custom clocks/entropy injection.
…e decorator

Removes the obsolete `validate_type` generic class/decorator wrapper from all
`ULID` class constructors and inlines clean, high-performance `isinstance` checks.

Why this change was made:
1. Architectural depth: Replaced a shallow decorator module with direct, readable
   runtime type validation, satisfying "Proposal 2" of the architectural review.
2. Performance & Debuggability: Eliminates wrapper function overhead in critical paths and
   keeps stack traces clean during exception reporting.
3. Clean namespace: Safely removed obsolete `Generic` and `TypeVar` typing imports.

Preserves exact backwards compatible TypeError messages for all validation paths.
Introduces a swappable `MonotonicityPolicy` interface (Protocol) to configure
and vary the generator's behavior during same-millisecond collisions.

Implementations added:
1. `StrictMonotonicPolicy`: Always increments the randomness by 1 (default), raising
   ValueError if the 80-bit randomness is exhausted.
2. `PureRandomPolicy`: Completely ignores monotonicity and always returns fresh,
   high-entropy random bytes, maximizing security and unpredictability.
3. `LaxMonotonicPolicy`: Attempts to increment monotonically, but on randomness
   exhaustion, regenerates fresh randomness instead of raising errors or sleeping.

Other Changes:
- Exposes direct type-assertion checks in the test suite to safely verify isolated states.
- Appended robust unit tests verifying the correctness of `PureRandomPolicy` and
  `LaxMonotonicPolicy` under collision and overflow constraints.
- Updated `CONTEXT.md` to formally document swappable Monotonicity Policies.
…ator

Consolidate ULID generation behind a single ULIDGenerator seam and expose it
as the public surface for customizing generation:

- Remove the per-call `policy` keyword from ULID(), from_timestamp,
  from_datetime and parse. The value object now delegates to a module-level,
  reassignable `default_generator`; callers wanting a custom policy build a
  ULIDGenerator and call generate().
- Collapse timestamp coercion into ULIDGenerator._normalize_timestamp so that
  generate() is the sole normalizer (datetime/int/float/None -> ms), with the
  narrow TypeError guards kept on the ULID.from_* constructors.
- Narrow the MonotonicityPolicy interface: drop the increment_bytes parameter
  and move the increment into BaseMonotonicPolicy._increment.
- Document ULIDGenerator, the monotonicity policies and default_generator in
  the API reference and add a "Generators and policies" section to the README.
- Add 4.0.0 (breaking: ValueProvider/ULID.provider removed) and 3.2.1
  changelog entries, and record the Keep a Changelog convention in AGENTS.md.
@sonarqubecloud

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.82716% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.65%. Comparing base (7a31bbd) to head (00c8e18).

Files with missing lines Patch % Lines
ulid/__init__.py 93.82% 3 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #65      +/-   ##
==========================================
- Coverage   99.41%   98.65%   -0.76%     
==========================================
  Files           4        4              
  Lines         342      372      +30     
  Branches       53       61       +8     
==========================================
+ Hits          340      367      +27     
- Misses          1        3       +2     
- Partials        1        2       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mdomke
mdomke merged commit aa8b06b into main Jul 20, 2026
10 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant