"""Implementation of threading.Thread with a return value on .join().

To use, assign the join to a variable. eg:

def thread_worker_kwarg(*, i: int = 0) -> tuple[int, int]:
    return (i + 1, i + 2)

rt = ReturnableThread[tuple[int, int]](target=thread_worker_kwarg, kwargs={"i": 15})
rt.start()
a, b = rt.join()
assert a == 16
assert b == 17
"""

from __future__ import annotations

import threading
from collections.abc import Callable, Iterable, Mapping
from typing import Any, Generic, TypeVar

T = TypeVar("T")
C = Callable[..., T]


class ReturnableThread(threading.Thread, Generic[T]):  # noqa: UP046 # 3.12 compat
    """Thread that returns a value (T) from the worker (Callable[...,T]) on join.

    Meant to provide a quick accessor to results from threads.
    """

    def __init__(  # noqa: D107 # inherit docstring
        self,
        target: C,
        args: None | Iterable[Any] = None,
        kwargs: None | Mapping[str, Any] = None,
    ):
        self._target = target
        self._args = args if args is not None else ()
        self._kwargs = kwargs
        self._return: T | None = None
        super().__init__(target=target, args=self._args, kwargs=kwargs)

    def run(self) -> None:  # noqa: D102 # inherit docstring
        if self._target is not None:
            if self._kwargs is None:
                self._return = self._target(*self._args)
            else:
                self._return = self._target(*self._args, **self._kwargs)

    def join(self, *args) -> T:  # type:ignore[override] # noqa: D102
        super().join(*args)
        if self._return is None:
            raise ValueError("Thread function has not been run (rt.start())")
        return self._return
