Skip to content

George's Utils

I made some utils and put them in a centralized place.

Install with uv pip install georges-utils.

utils

AutoId

Bases: Generic[T]

A class that has a global id that can be reset and incremented. It defaults to using the natural numbers (0, 1, 2, ...) but this can be overridden. If the type parameter is changed, the _increment_id and _id_default_value methods need to be updated.

Source code in utils/auto_id.py
class AutoId(Generic[T]):
    """
    A class that has a global id that can be reset and incremented.
    It defaults to using the natural numbers (0, 1, 2, ...) but this can be overridden.
    If the type parameter is changed, the `_increment_id` and `_id_default_value` methods need to be updated.
    """

    _global_id: ClassVar[T] = cast(T, 0)

    def __init_subclass__(cls, **kwargs: object) -> None:
        super().__init_subclass__(**kwargs)
        cls.reset_global_id()

    @classmethod
    def _increment_id(cls, id: T, /) -> T:
        """
        Transforms the current id to the next one.
        The default is to increment it.
        """
        return id + 1  # type:ignore

    @classmethod
    def next_id(cls) -> T:
        """
        Generate the next id.
        The default behavior is to increment the current id (and return the old one).
        However, this can be changed by overriding `_increment_global_id`.
        """
        id = cls._global_id
        cls._global_id = cls._increment_id(cls._global_id)
        return id

    @classmethod
    def _id_default_value(cls) -> T:
        """
        The initial and default value for the global id.
        This is set when the class is created or when `reset_global_id` is called.
        The default value is 0.
        """
        return cast(T, 0)

    @classmethod
    def reset_global_id(cls) -> None:
        """
        Reset the global id of this class to the default.
        It does not affect subclasses.
        """
        cls._global_id = cls._id_default_value()

    @classmethod
    def _reset_all_global_ids(cls) -> None:
        """
        Reset the global ids of this class and all subclasses.
        This is used by the pytest fixture to ensure all subclasses are updated.
        """
        cls.reset_global_id()
        for subclass in cls.__subclasses__():
            subclass._reset_all_global_ids()

next_id() classmethod

Generate the next id. The default behavior is to increment the current id (and return the old one). However, this can be changed by overriding _increment_global_id.

Source code in utils/auto_id.py
@classmethod
def next_id(cls) -> T:
    """
    Generate the next id.
    The default behavior is to increment the current id (and return the old one).
    However, this can be changed by overriding `_increment_global_id`.
    """
    id = cls._global_id
    cls._global_id = cls._increment_id(cls._global_id)
    return id

reset_global_id() classmethod

Reset the global id of this class to the default. It does not affect subclasses.

Source code in utils/auto_id.py
@classmethod
def reset_global_id(cls) -> None:
    """
    Reset the global id of this class to the default.
    It does not affect subclasses.
    """
    cls._global_id = cls._id_default_value()

todo(*args)

Raises a NotImplementedYetError, which is a subclass of a NotImplementedError. Any additional args will be passed into the error.

Source code in utils/errors.py
def todo(*args: object) -> NoReturn:
    """
    Raises a `NotImplementedYetError`, which is a subclass of a `NotImplementedError`.
    Any additional args will be passed into the error.
    """
    raise NotImplementedYetError(*args)

unimplemented(*args)

Raises a NotImplemented error. Any additional args will be passed into the error.

Source code in utils/errors.py
def unimplemented(*args: object) -> NoReturn:
    """
    Raises a `NotImplemented` error.
    Any additional args will be passed into the error.
    """
    raise NotImplementedError(*args)

unreachable(*args)

Raises an UnreachableError, which is a subclass of an AssertionError. Any additional args will be passed into the error.

Source code in utils/errors.py
def unreachable(*args: object) -> NoReturn:
    """
    Raises an `UnreachableError`, which is a subclass of an `AssertionError`.
    Any additional args will be passed into the error.
    """
    raise UnreachableError(*args)

utils.NotImplementedYetError

Bases: NotImplementedError

Source code in utils/errors.py
class NotImplementedYetError(NotImplementedError): ...

utils.UnreachableError

Bases: AssertionError

Source code in utils/errors.py
class UnreachableError(AssertionError): ...

utils.cached_iterator

cached_iterator(fn)

Cache an iterator by storing all the resulting elements as a tuple. This works as on plain functions, or as an instance method, property, classmethod or other descriptor. This uses functools.lru_cache to implement the cache and so methods such as cache_clear are still valid.

Example use:

from typing import Generator
from utils import cached_iterator

@cached_iterator
def squares(n: int) -> Generator[int]:
    for i in range(1, n + 1):
        yield i ** 2

assert squares(5) == (1, 4, 9, 16, 25)
assert squares(5) is squares(5) # cached

Source code in utils/cached_iterator.py
def cached_iterator[**P, T](fn: Callable[P, Iterable[T]]) -> Callable[P, Sequence[T]]:
    """
    Cache an iterator by storing all the resulting elements as a tuple.
    This works as on plain functions, or as an instance method, `property`, `classmethod` or other descriptor.
    This uses `functools.lru_cache` to implement the cache and so methods such as `cache_clear` are still valid.

    Example use:
    ```python
    from typing import Generator
    from utils import cached_iterator

    @cached_iterator
    def squares(n: int) -> Generator[int]:
        for i in range(1, n + 1):
            yield i ** 2

    assert squares(5) == (1, 4, 9, 16, 25)
    assert squares(5) is squares(5) # cached
    ```
    """
    return cast(
        Callable[P, Sequence[T]],
        functools.lru_cache(typed=True)(compose(_CACHED_ITERATOR_TYPE, fn)),
    )

utils.compose

compose(f, g)

Combine functions f and g to f.g where f.g(x) = f(g(x)).

Source code in utils/compose.py
4
5
6
def compose[**P, T, U](f: Callable[[T], U], g: Callable[P, T], /) -> Callable[P, U]:
    """Combine functions `f` and `g` to `f.g` where `f.g(x) = f(g(x))`."""
    return lambda *args, **kwargs: f(g(*args, **kwargs))

utils.identity

identity(x)

Return the value unchanged.

Source code in utils/identity.py
4
5
6
def identity[T](x: T, /) -> Unmodified[T]:
    """Return the value unchanged."""
    return x

utils.increment

increment(x)

Add one to a number.

Source code in utils/increment.py
1
2
3
def increment(x: float | int, /) -> float:
    """Add one to a number."""
    return x + 1

utils.decrement

decrement(x)

Subtract one from a number.

Source code in utils/decrement.py
1
2
3
def decrement(x: float | int, /) -> float:
    """Subtract one from a number."""
    return x - 1

utils.min_max

min_max(a, b, /, *, key=None)

min_max(
    a: T, b: T, /, *, key: None = None
) -> tuple[Unmodified[T], Unmodified[T]]
min_max(
    a: T, b: T, /, *, key: Callable[[T], U]
) -> tuple[Unmodified[T], Unmodified[T]]

Return the min and the max of two items as a tuple. The first item in the min and the second is the max. It is also possible to specify a key which is used for comparison. In the case of a tie, the original order is maintained. For example:

>>> from utils import min_max
>>> min_max(0, 5)
(0, 5)
>>> min_max(0.5, 0.1)
(0.1, 0.5)
>>> import operator
>>> min_max(10, 5, key=operator.neg)  # compare by lambda x: -x
(10, 5)
>>> min_max("gfed", "cba", key=lambda _: 0)
('cfed', 'cba')

Source code in utils/min_max.py
def min_max(a: Any, b: Any, /, *, key: Callable[[Any], Any] | None = None) -> tuple[Any, Any]:
    """
    Return the min and the max of two items as a tuple.
    The first item in the min and the second is the max.
    It is also possible to specify a key which is used for comparison.
    In the case of a tie, the original order is maintained.
    For example:
    ```python
    >>> from utils import min_max
    >>> min_max(0, 5)
    (0, 5)
    >>> min_max(0.5, 0.1)
    (0.1, 0.5)
    >>> import operator
    >>> min_max(10, 5, key=operator.neg)  # compare by lambda x: -x
    (10, 5)
    >>> min_max("gfed", "cba", key=lambda _: 0)
    ('cfed', 'cba')
    ```
    """
    if key is None:
        key = identity
    if key(b) < key(a):
        return b, a
    return a, b

utils.round

round(x)

Round the value to the nearest integer. When n is a natural number, - n + 0.5 rounds to n + 1 - -n - 0.5 rounds to -n For example:

>>> from utils import round
>>> round(4.5)
5
>>> round(-4.5)
4
>>> round(0.5)
1
>>> round(-0.5)
0
You must import this function as it has different behavior to the builtins.round.

Source code in utils/round.py
def round(x: int | float, /) -> int:
    """
    Round the value to the nearest integer.
    When n is a natural number,
    - `n + 0.5` rounds to `n + 1`
    - `-n - 0.5` rounds to `-n`
    For example:
    ```python
    >>> from utils import round
    >>> round(4.5)
    5
    >>> round(-4.5)
    4
    >>> round(0.5)
    1
    >>> round(-0.5)
    0
    ```
    You must import this function as it has different behavior to the `builtins.round`.
    """
    return math.floor(x + 0.5)

utils.plugins.pytest_plugin

This contains a collection of helpful pytest fixtures. They are automatically loaded when this module is installed.

filepath(filename)

Convert a filename parameter as a string into a Path. For example:

import pytest
from pathlib import Path
@pytest.mark.parametrize(
    "filename, contents",
    [
        ("log.txt", "log_data"),
        ("scores.json", '{"player_1": 1, "player_2": 2}'),
    ]
)
def test_simplification(filepath: Path, contents: str) -> None:
    assert filepath.read_text() == contents

Source code in utils/plugins/pytest_plugin.py
@pytest.fixture
def filepath(filename: str) -> Path:
    """
    Convert a filename parameter as a string into a `Path`.
    For example:
    ```python
    import pytest
    from pathlib import Path
    @pytest.mark.parametrize(
        "filename, contents",
        [
            ("log.txt", "log_data"),
            ("scores.json", '{"player_1": 1, "player_2": 2}'),
        ]
    )
    def test_simplification(filepath: Path, contents: str) -> None:
        assert filepath.read_text() == contents
    ```
    """
    return Path(filename)

manual_seed(seed)

Automatically seed random generators. Use this by defining the seed parameter or fixture. In this example, each test has a different seed:

import pytest, random
@pytest.mark.parametrize("seed", range(5))
def test_random_generation(seed: int) -> None:
    result = [random.random() for _ in range(10)]
If no seed is specified, it defaults to 0. This currently seeds the standard library, numpy and pytorch random libraries.

Source code in utils/plugins/pytest_plugin.py
@pytest.fixture(autouse=True)
def manual_seed(seed: int) -> int:
    """
    Automatically seed random generators.
    Use this by defining the seed parameter or fixture.
    In this example, each test has a different seed:
    ```python
    import pytest, random
    @pytest.mark.parametrize("seed", range(5))
    def test_random_generation(seed: int) -> None:
        result = [random.random() for _ in range(10)]
    ```
    If no seed is specified, it defaults to 0.
    This currently seeds the standard library, numpy and pytorch random libraries.
    """
    random.seed(seed)
    try:
        import numpy as np
    except ImportError:
        ...
    else:
        np.random.seed(seed)
    try:
        import torch as th  # type:ignore[import-not-found]
    except ImportError:
        ...
    else:
        th.random.manual_seed(seed)
    return seed

reset_global_id()

Reset all AutoId subclasses between each test.

Source code in utils/plugins/pytest_plugin.py
@pytest.fixture(autouse=True)
def reset_global_id() -> None:
    """Reset all `AutoId` subclasses between each test."""
    AutoId._reset_all_global_ids()

setup_debug()

Setup dbg. This will put dbg into builtins so it can be used when testing without an import. It also sorts any unordered collections to make understanding test failures easier when using pytest-dbg.

Source code in utils/plugins/pytest_plugin.py
@pytest.fixture(autouse=True, scope="session")
def setup_debug() -> None:
    """
    Setup [dbg](https://github.com/George-Ogden/dbg).
    This will put `dbg` into `builtins` so it can be used when testing without an import.
    It also sorts any unordered collections to make understanding test failures easier when using [pytest-dbg](https://github.com/George-Ogden/pytest-dbg/).
    """
    try:
        from debug import CONFIG, install
    except ImportError:
        ...
    else:
        install()
        CONFIG.sort_unordered_collections = True

test_dir(test_path)

Returns the path of directory of the test that is being set up.

Source code in utils/plugins/pytest_plugin.py
@pytest.fixture
def test_dir(test_path: Path) -> Path:
    """Returns the path of directory of the test that is being set up."""
    return test_path.parent

test_path(request)

Returns the path of the test that is being set up.

Source code in utils/plugins/pytest_plugin.py
@pytest.fixture
def test_path(request: pytest.FixtureRequest) -> Path:
    """Returns the path of the test that is being set up."""
    return request.path