Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# Changelog

## Unreleased

- Keep `UniqueList` membership in sync when replacing an indexed item, and
leave membership unchanged when the index is out of range, contributed by
@shkyyy18 in PR #51.
- Keep `UniqueList` membership in sync for slice assignment, `extend`, `pop`,
`remove`, `clear`, `+=` and `*=`, so `extend` and `+=` no longer add
duplicates and removed values can be added again.
- Accept one-shot iterables in `UniqueList` slice assignment, allow a slice to
reuse the values it replaces, and reject a slice that repeats a value.
- Make `copy.copy` and `copy.deepcopy` of a `UniqueList` return a working copy
with its own membership.
- Leave `UniqueList` membership unchanged when `insert` fails, and answer `in`
for an unhashable value the way a `list` does.
- Make `CastedDict` and `LazyCastedDict` load from `pickle` on every protocol,
including pickles written by 4.0.1, and stop `copy.copy` and `copy.deepcopy`
from casting the stored values a second time.
- Cast what `setdefault` and `|=` store in a `CastedDict` or `LazyCastedDict`.
A `None` default is stored as it is.
- Cast the key of a `LazyCastedDict` once when it is stored. It was cast twice.
- Let keyword arguments win over the mapping in `update` and the constructor
of the casted dicts, as `dict` does.
- Make `!=` the opposite of `==` for `SliceableDeque`, and compare unequal to a
set when an item is unhashable.
- Remove the iterable of mappings from the `DictUpdateArgs` type alias. The
code never accepted that shape.

## 4.0.1 - 2026-08-30

- Allow `uv_build` 0.12.x, contributed by @felixonmars in PR #49.
Expand Down
56 changes: 56 additions & 0 deletions _python_utils_tests/clock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""A fake clock for the tests and doctests of ``python_utils.time``."""

import typing

import pytest

import python_utils.time

#: The doctest that sleeps on the clock, by the name pytest gives it.
TIMEOUT_GENERATOR_DOCTEST: str = 'python_utils.time.timeout_generator'


class FakeClock:
"""
A clock that only moves when something sleeps on it.

``time.sleep`` promises to sleep at least as long as requested. A busy
machine sleeps tens of milliseconds longer, and that changes how many
items ``timeout_generator`` yields before its timeout. On this clock the
number of items depends on the arguments alone.

Attributes:
now (float): The current time in seconds.
sleeps (list[float]): Every requested sleep, in order.
"""

def __init__(self) -> None:
"""Start at zero without any recorded sleeps."""
self.now: float = 0.0
self.sleeps: list[float] = []

def perf_counter(self) -> float:
"""Return the current time, like ``time.perf_counter``."""
return self.now

def sleep(self, seconds: float) -> None:
"""Record the sleep and move the clock forward, without waiting."""
self.sleeps.append(seconds)
self.now += seconds


@pytest.fixture
def fake_clock(monkeypatch: pytest.MonkeyPatch) -> FakeClock:
"""Replace the ``time`` module inside ``python_utils.time``."""
clock: FakeClock = FakeClock()
monkeypatch.setattr(python_utils.time, 'time', clock)
return clock


@pytest.fixture(autouse=True)
def fake_clock_in_doctest(request: pytest.FixtureRequest) -> None:
"""Run the ``timeout_generator`` doctest on the fake clock."""
# pytest leaves `FixtureRequest.node` without a type.
node: pytest.Item = typing.cast(pytest.Item, request.node)
if node.name == TIMEOUT_GENERATOR_DOCTEST:
request.getfixturevalue('fake_clock')
Loading
Loading