From 9e2d932dbe5b3f14f58200a9f5094348f2fa9938 Mon Sep 17 00:00:00 2001 From: ErickLTrentini Date: Fri, 23 Jan 2026 22:50:34 -0300 Subject: [PATCH 1/6] refactor: simplify multi-argument transformer logic - Remove specialized MultiArgsTransformer and MultiArgsAsyncTransformer classes, consolidating their functionality into the base Transformer and AsyncTransformer classes using a `_multi_args` flag. - Update composition utilities to handle multi-argument flows without requiring explicit sub-class checks. - Add AGENTS.md to define development standards, including the use of `uv`, strong typing, and Sphinx-style docstrings. - Clean up public API exports in `gloe/__init__.py`. - Improve test robustness by adding conditional skips for optional dependencies like pygraphviz. --- AGENTS.md | 16 + gloe/__init__.py | 6 +- gloe/_composition_utils.py | 143 +++---- gloe/async_transformer.py | 144 +++---- gloe/base_transformer.py | 1 + gloe/functional.py | 18 +- gloe/transformers.py | 200 +++------- pyproject.toml | 7 +- .../test_multiargs_transformer_types.py | 10 +- .../partial/test_partial_transformer_types.py | 3 +- tests/test_transformer_export.py | 8 + uv.lock | 364 ++++++++++++++++++ 12 files changed, 562 insertions(+), 358 deletions(-) create mode 100644 AGENTS.md create mode 100644 uv.lock diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..09e508e6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,16 @@ +# AGENTS + +## General +- Use `uv` as the default package manager and runner. +- Keep code simple and self-documenting; do not add random comments. + +## Typing +- Use strong, explicit typing everywhere. +- This library is mypy-heavy; ensure all code type-checks cleanly. + +## Documentation pattern +- Follow the existing docstring style used in the codebase (reST/Sphinx style). +- Prefer structured sections like `Args:`, `Returns:`, `Raises:`, `Example:`, and `See Also:` when documenting. + +## Testing +- Always run the full test suite at the end of a task (use `uv`). \ No newline at end of file diff --git a/gloe/__init__.py b/gloe/__init__.py index 1a976b62..10562657 100644 --- a/gloe/__init__.py +++ b/gloe/__init__.py @@ -8,10 +8,10 @@ from gloe.conditional import If, condition from gloe.ensurer import ensure from gloe.exceptions import UnsupportedTransformerArgException -from gloe.transformers import Transformer, MultiArgsTransformer +from gloe.transformers import Transformer from gloe.base_transformer import BaseTransformer, PreviousTransformer from gloe.base_transformer import TransformerException -from gloe.async_transformer import AsyncTransformer, MultiArgsAsyncTransformer +from gloe.async_transformer import AsyncTransformer __version__ = "0.7.0" @@ -33,5 +33,3 @@ setattr(Transformer, "__rshift__", _compose_nodes) setattr(AsyncTransformer, "__rshift__", _compose_nodes) -setattr(MultiArgsTransformer, "__rshift__", _compose_nodes) -setattr(MultiArgsAsyncTransformer, "__rshift__", _compose_nodes) diff --git a/gloe/_composition_utils.py b/gloe/_composition_utils.py index 4f35e5e6..7b759220 100644 --- a/gloe/_composition_utils.py +++ b/gloe/_composition_utils.py @@ -2,10 +2,10 @@ from inspect import Signature from typing import TypeVar, Any, Optional, Union -from gloe.async_transformer import AsyncTransformer, MultiArgsAsyncTransformer +from gloe.async_transformer import AsyncTransformer from gloe.base_transformer import BaseTransformer from gloe.gateways._parallel import _Parallel, _ParallelAsync -from gloe.transformers import Transformer, MultiArgsTransformer +from gloe.transformers import Transformer from gloe._typing_utils import _match_types, _specify_types from gloe.exceptions import UnsupportedTransformerArgException @@ -20,6 +20,10 @@ def is_transformer(node): return isinstance(node, Transformer) +def _is_multi_args(transformer: BaseTransformer) -> bool: + return getattr(transformer, "_multi_args", False) + + def _resolve_serial_connection_signatures( transformer1: BaseTransformer, transformer2: BaseTransformer, generic_vars: dict ) -> Signature: @@ -76,55 +80,28 @@ def __len__(self): new_transformer: Optional[BaseTransformer] = None if is_transformer(transformer1) and is_transformer(transformer2): - if isinstance(transformer1, MultiArgsTransformer): - - class NewMultiArgsTransformer(BaseNewTransformer, MultiArgsTransformer): - def __init__(self): - super().__init__() - self._flow = transformer1._flow + transformer2._flow - - def transform(self, data): - return None - - new_transformer = NewMultiArgsTransformer() - - else: - - class NewTransformer1(BaseNewTransformer, Transformer[_In, _NextOut]): - def __init__(self): - super().__init__() - self._flow = transformer1._flow + transformer2._flow + class NewTransformer1(BaseNewTransformer, Transformer[_In, _NextOut]): + def __init__(self): + super().__init__() + self._flow = transformer1._flow + transformer2._flow - def transform(self, data): - return None + def transform(self, data): + return None - new_transformer = NewTransformer1() + new_transformer = NewTransformer1() + new_transformer._multi_args = _is_multi_args(transformer1) else: - if isinstance(transformer1, MultiArgsAsyncTransformer): - - class NewMultiArgsAsyncTransformer( - BaseNewTransformer, MultiArgsAsyncTransformer - ): - def __init__(self): - super().__init__() - self._flow = transformer1._flow + transformer2._flow - - async def transform_async(self, data): - return None - - new_transformer = NewMultiArgsAsyncTransformer() - else: + class NewTransformer2(BaseNewTransformer, AsyncTransformer[_In, _NextOut]): + def __init__(self): + super().__init__() + self._flow = transformer1._flow + transformer2._flow - class NewTransformer2(BaseNewTransformer, AsyncTransformer[_In, _NextOut]): - def __init__(self): - super().__init__() - self._flow = transformer1._flow + transformer2._flow + async def transform_async(self, data): + return None - async def transform_async(self, data): - return None - - new_transformer = NewTransformer2() + new_transformer = NewTransformer2() + new_transformer._multi_args = _is_multi_args(transformer1) new_transformer.__class__.__name__ = transformer2.__class__.__name__ new_transformer._label = transformer2.label @@ -157,67 +134,35 @@ def __len__(self): if is_transformer(incident_transformer) and is_transformer(receiving_transformers): - if isinstance(incident_transformer, MultiArgsTransformer): - - class NewMultiArgsTransformer(BaseNewTransformer, MultiArgsTransformer): - def __init__(self): - super().__init__() - self._flow = incident_transformer._flow + [ - _Parallel(*receiving_transformers) - ] - - def transform(self, data): - return None - - new_transformer = NewMultiArgsTransformer() - else: - - class NewTransformer1( - BaseNewTransformer, Transformer[_In, tuple[Any, ...]] - ): - def __init__(self): - super().__init__() - self._flow = incident_transformer._flow + [ - _Parallel(*receiving_transformers) - ] + class NewTransformer1(BaseNewTransformer, Transformer[_In, tuple[Any, ...]]): + def __init__(self): + super().__init__() + self._flow = incident_transformer._flow + [ + _Parallel(*receiving_transformers) + ] - def transform(self, data): - return None + def transform(self, data): + return None - new_transformer = NewTransformer1() + new_transformer = NewTransformer1() + new_transformer._multi_args = _is_multi_args(incident_transformer) else: - if isinstance(incident_transformer, MultiArgsAsyncTransformer): - - class NewMultiArgsAsyncTransformer( - BaseNewTransformer, MultiArgsAsyncTransformer - ): - def __init__(self): - super().__init__() - self._flow = incident_transformer._flow + [ - _ParallelAsync(*receiving_transformers) - ] - - async def transform_async(self, data): - return None - - new_transformer = NewMultiArgsAsyncTransformer() - else: - - class NewTransformer2( - BaseNewTransformer, AsyncTransformer[_In, tuple[Any, ...]] - ): - def __init__(self): - super().__init__() - self._flow = incident_transformer._flow + [ - _ParallelAsync(*receiving_transformers) - ] + class NewTransformer2( + BaseNewTransformer, AsyncTransformer[_In, tuple[Any, ...]] + ): + def __init__(self): + super().__init__() + self._flow = incident_transformer._flow + [ + _ParallelAsync(*receiving_transformers) + ] - async def transform_async(self, data): - return None + async def transform_async(self, data): + return None - new_transformer = NewTransformer2() + new_transformer = NewTransformer2() + new_transformer._multi_args = _is_multi_args(incident_transformer) # new_transformer._previous = cast(Transformer, receiving_transformers) new_transformer.__class__.__name__ = "Converge" diff --git a/gloe/async_transformer.py b/gloe/async_transformer.py index bd4d7008..7521518d 100644 --- a/gloe/async_transformer.py +++ b/gloe/async_transformer.py @@ -1,16 +1,19 @@ from abc import abstractmethod from inspect import Signature -from typing import TypeVar, overload, cast, Callable, Optional, Any +from typing import TypeVar, overload, cast, Callable, Optional, Any, get_args, get_origin, TYPE_CHECKING -from typing_extensions import Self, Unpack, Generic, TypeVarTuple, override +from typing_extensions import Self, Unpack, Generic, TypeVarTuple from gloe._plotting_utils import PlottingSettings, NodeType from gloe._transformer_utils import catch_transformer_exception from gloe.base_transformer import BaseTransformer, Flow +from gloe.exceptions import TransformerRequiresMultiArgs + +if TYPE_CHECKING: + from gloe.transformers import Transformer __all__ = ["AsyncTransformer"] -_In = TypeVar("_In", contravariant=True) _Out = TypeVar("_Out", covariant=True) _NextOut = TypeVar("_NextOut") @@ -22,6 +25,7 @@ _O7 = TypeVar("_O7") Args = TypeVarTuple("Args") +NextArgs = TypeVarTuple("NextArgs") async def _execute_async_flow(flow: Flow, arg: Any) -> Any: @@ -40,7 +44,7 @@ async def _execute_async_flow(flow: Flow, arg: Any) -> Any: return result -class AsyncTransformer(Generic[_In, _Out], BaseTransformer[_In, _Out]): +class AsyncTransformer(Generic[Unpack[Args], _Out], BaseTransformer[Any, _Out]): def __init__(self): super().__init__() @@ -48,9 +52,15 @@ def __init__(self): node_type=NodeType.Transformer, is_async=True ) self.__class__.__annotations__ = self.transform_async.__annotations__ + orig_bases = getattr(self, "__orig_bases__", []) + transformer_args = [ + get_args(base) for base in orig_bases if get_origin(base) == AsyncTransformer + ] + if transformer_args and len(transformer_args[0]) > 2: + self._multi_args = True @abstractmethod - async def transform_async(self, data: _In) -> _Out: + async def transform_async(self, data: Any) -> _Out: """ Method to perform the transformation asynchronously. @@ -79,7 +89,7 @@ def __repr__(self): f" -> {self.output_annotation}" ) - async def _safe_transform(self, data: _In) -> _Out: + async def _safe_transform(self, data: Any) -> _Out: transform_exception = None transformed: Optional[_Out] = None @@ -98,7 +108,7 @@ async def _safe_transform(self, data: _In) -> _Out: def copy( self, - transform: Optional[Callable[[Self, _In], _Out]] = None, + transform: Optional[Callable[[Self, Any], _Out]] = None, regenerate_instance_id: bool = False, force: bool = False, ) -> Self: @@ -109,114 +119,62 @@ async def __call__(self: "AsyncTransformer[None, _Out]") -> _Out: return await _execute_async_flow(self._flow, None) @overload - async def __call__(self, data: _In) -> _Out: - return await _execute_async_flow(self._flow, data) - - async def __call__(self, data=None): + async def __call__( + self: "AsyncTransformer[Unpack[Args], _Out]", *data: Unpack[Args] + ) -> _Out: return await _execute_async_flow(self._flow, data) - @overload - def __rshift__( - self, next_node: BaseTransformer[_Out, _NextOut] - ) -> "AsyncTransformer[_In, _NextOut]": - pass - - @overload - def __rshift__( - self, - next_node: tuple[BaseTransformer[_Out, _NextOut], BaseTransformer[_Out, _O2]], - ) -> "AsyncTransformer[_In, tuple[_NextOut, _O2]]": - pass + async def __call__(self, *data): + if self._multi_args: + if len(data) == 1 and type(data[0]) is tuple: + data = data[0] + elif len(data) == 1: + raise TransformerRequiresMultiArgs() + return await _execute_async_flow(self._flow, data) - @overload - def __rshift__( - self, - next_node: tuple[ - BaseTransformer[_Out, _NextOut], - BaseTransformer[_Out, _O2], - BaseTransformer[_Out, _O3], - ], - ) -> "AsyncTransformer[_In, tuple[_NextOut, _O2, _O3]]": - pass + if len(data) == 0: + return await _execute_async_flow(self._flow, None) + if len(data) == 1: + return await _execute_async_flow(self._flow, data[0]) + raise TypeError("Transformer expected a single argument") @overload def __rshift__( - self, - next_node: tuple[ - BaseTransformer[_Out, _NextOut], - BaseTransformer[_Out, _O2], - BaseTransformer[_Out, _O3], - BaseTransformer[_Out, _O4], - ], - ) -> "AsyncTransformer[_In, tuple[_NextOut, _O2, _O3, _O4]]": + self: "AsyncTransformer[Unpack[Args], tuple[Unpack[NextArgs]]]", + next_node: "Transformer[Unpack[NextArgs], _NextOut]", + ) -> "AsyncTransformer[Unpack[Args], _NextOut]": pass @overload def __rshift__( - self, - next_node: tuple[ - BaseTransformer[_Out, _NextOut], - BaseTransformer[_Out, _O2], - BaseTransformer[_Out, _O3], - BaseTransformer[_Out, _O4], - BaseTransformer[_Out, _O5], - ], - ) -> "AsyncTransformer[_In, tuple[_NextOut, _O2, _O3, _O4, _O5]]": + self: "AsyncTransformer[Unpack[Args], tuple[Unpack[NextArgs]]]", + next_node: "AsyncTransformer[Unpack[NextArgs], _NextOut]", + ) -> "AsyncTransformer[Unpack[Args], _NextOut]": pass @overload def __rshift__( - self, - next_node: tuple[ - BaseTransformer[_Out, _NextOut], - BaseTransformer[_Out, _O2], - BaseTransformer[_Out, _O3], - BaseTransformer[_Out, _O4], - BaseTransformer[_Out, _O5], - BaseTransformer[_Out, _O6], - ], - ) -> "AsyncTransformer[_In, tuple[_NextOut, _O2, _O3, _O4, _O5, _O6]]": + self, next_node: "Transformer[_Out, _NextOut]" + ) -> "AsyncTransformer[Unpack[Args], _NextOut]": pass @overload def __rshift__( - self, - next_node: tuple[ - BaseTransformer[_Out, _NextOut], - BaseTransformer[_Out, _O2], - BaseTransformer[_Out, _O3], - BaseTransformer[_Out, _O4], - BaseTransformer[_Out, _O5], - BaseTransformer[_Out, _O6], - BaseTransformer[_Out, _O7], - ], - ) -> "AsyncTransformer[_In, tuple[_NextOut, _O2, _O3, _O4, _O5, _O6, _O7]]": - pass - - def __rshift__(self, next_node): # pragma: no cover + self, next_node: "AsyncTransformer[_Out, _NextOut]" + ) -> "AsyncTransformer[Unpack[Args], _NextOut]": pass - -class MultiArgsAsyncTransformer( - Generic[Unpack[Args], _Out], AsyncTransformer[tuple[Unpack[Args]], _Out] -): - @override - async def __call__( # type: ignore[override] - self: "MultiArgsAsyncTransformer[Unpack[Args], _Out]", *data: Unpack[Args] - ) -> _Out: - return await _execute_async_flow(self._flow, data) - @overload def __rshift__( self, next_node: BaseTransformer[_Out, _NextOut] - ) -> "MultiArgsAsyncTransformer[Unpack[Args], _NextOut]": + ) -> "AsyncTransformer[Unpack[Args], _NextOut]": pass @overload def __rshift__( self, next_node: tuple[BaseTransformer[_Out, _NextOut], BaseTransformer[_Out, _O2]], - ) -> "MultiArgsAsyncTransformer[Unpack[Args], tuple[_NextOut, _O2]]": + ) -> "AsyncTransformer[Unpack[Args], tuple[_NextOut, _O2]]": pass @overload @@ -227,7 +185,7 @@ def __rshift__( BaseTransformer[_Out, _O2], BaseTransformer[_Out, _O3], ], - ) -> "MultiArgsAsyncTransformer[Unpack[Args], tuple[_NextOut, _O2, _O3]]": + ) -> "AsyncTransformer[Unpack[Args], tuple[_NextOut, _O2, _O3]]": pass @overload @@ -239,7 +197,7 @@ def __rshift__( BaseTransformer[_Out, _O3], BaseTransformer[_Out, _O4], ], - ) -> "MultiArgsAsyncTransformer[Unpack[Args], tuple[_NextOut, _O2, _O3, _O4]]": + ) -> "AsyncTransformer[Unpack[Args], tuple[_NextOut, _O2, _O3, _O4]]": pass @overload @@ -252,7 +210,7 @@ def __rshift__( BaseTransformer[_Out, _O4], BaseTransformer[_Out, _O5], ], - ) -> "MultiArgsAsyncTransformer[Unpack[Args], tuple[_NextOut, _O2, _O3, _O4, _O5]]": + ) -> "AsyncTransformer[Unpack[Args], tuple[_NextOut, _O2, _O3, _O4, _O5]]": pass @overload @@ -266,9 +224,7 @@ def __rshift__( BaseTransformer[_Out, _O5], BaseTransformer[_Out, _O6], ], - ) -> """MultiArgsAsyncTransformer[ - Unpack[Args], tuple[_NextOut, _O2, _O3, _O4, _O5, _O6] - ]""": + ) -> "AsyncTransformer[Unpack[Args], tuple[_NextOut, _O2, _O3, _O4, _O5, _O6]]": pass @overload @@ -283,9 +239,7 @@ def __rshift__( BaseTransformer[_Out, _O6], BaseTransformer[_Out, _O7], ], - ) -> """MultiArgsAsyncTransformer[ - Unpack[Args], tuple[_NextOut, _O2, _O3, _O4, _O5, _O6, _O7] - ]""": + ) -> "AsyncTransformer[Unpack[Args], tuple[_NextOut, _O2, _O3, _O4, _O5, _O6, _O7]]": pass def __rshift__(self, next_node): # pragma: no cover diff --git a/gloe/base_transformer.py b/gloe/base_transformer.py index a85b204e..b9552a4f 100644 --- a/gloe/base_transformer.py +++ b/gloe/base_transformer.py @@ -101,6 +101,7 @@ def __init__(self): self.id = uuid.uuid4() self.instance_id = uuid.uuid4() self.is_atomic = False + self._multi_args = False self._label = self.__class__.__name__ self._already_copied = False self._plotting_settings: PlottingSettings = PlottingSettings( diff --git a/gloe/functional.py b/gloe/functional.py index c072cce8..21d40aad 100644 --- a/gloe/functional.py +++ b/gloe/functional.py @@ -5,9 +5,9 @@ from typing_extensions import Concatenate, TypeVarTuple, Unpack, ParamSpec -from gloe.async_transformer import AsyncTransformer, MultiArgsAsyncTransformer +from gloe.async_transformer import AsyncTransformer from gloe.exceptions import TransformerRequiresMultiArgs -from gloe.transformers import Transformer, MultiArgsTransformer +from gloe.transformers import Transformer __all__ = [ "transformer", @@ -156,7 +156,7 @@ async def transform_async(self, data: A) -> S: @overload def transformer( func: Callable[[A, B, Unpack[Rest]], S], -) -> MultiArgsTransformer[A, B, Unpack[Rest], S]: +) -> Transformer[A, B, Unpack[Rest], S]: pass @@ -194,7 +194,7 @@ def filter_subscribed_users(users: list[User]) -> list[User]: if len(func_signature.parameters) > 1: - class LambdaMultiArgsTransformer(MultiArgsTransformer): + class LambdaMultiArgsTransformer(Transformer): __doc__ = func.__doc__ __annotations__ = cast(FunctionType, func).__annotations__ @@ -209,9 +209,10 @@ def transform(self, data): raise NotImplementedError() # pragma: no cover lambda_transformer1 = LambdaMultiArgsTransformer() + lambda_transformer1._multi_args = True lambda_transformer1.__class__.__name__ = func.__name__ lambda_transformer1._label = func.__name__ - return lambda_transformer1 + return cast(Transformer, lambda_transformer1) class LambdaTransformer(Transformer): __doc__ = func.__doc__ @@ -234,7 +235,7 @@ def transform(self, data=None): @overload def async_transformer( func: Callable[[A, B, Unpack[Rest]], Awaitable[S]], -) -> MultiArgsAsyncTransformer[A, B, Unpack[Rest], S]: +) -> AsyncTransformer[A, B, Unpack[Rest], S]: pass @@ -274,7 +275,7 @@ async def get_user_by_role(role: str) -> list[User]: if len(func_signature.parameters) > 1: - class LambdaMultiArgsTransformer(MultiArgsAsyncTransformer): + class LambdaMultiArgsTransformer(AsyncTransformer): __doc__ = func.__doc__ __annotations__ = cast(FunctionType, func).__annotations__ @@ -289,9 +290,10 @@ async def transform_async(self, data): raise NotImplementedError() # pragma: no cover lambda_transformer1 = LambdaMultiArgsTransformer() + lambda_transformer1._multi_args = True lambda_transformer1.__class__.__name__ = func.__name__ lambda_transformer1._label = func.__name__ - return lambda_transformer1 + return cast(AsyncTransformer, lambda_transformer1) class LambdaAsyncTransformer(AsyncTransformer): __doc__ = func.__doc__ diff --git a/gloe/transformers.py b/gloe/transformers.py index 446b8cea..1c075273 100644 --- a/gloe/transformers.py +++ b/gloe/transformers.py @@ -1,13 +1,14 @@ from abc import ABC, abstractmethod from inspect import Signature -from typing import TypeVar, overload, cast, Optional, Any +from typing import TypeVar, overload, cast, Optional, Any, get_args, get_origin, Iterable -from typing_extensions import TypeAlias, Unpack, TypeVarTuple, Generic, override +from typing_extensions import Generic, TypeAlias, Unpack, TypeVarTuple -from gloe.async_transformer import AsyncTransformer, MultiArgsAsyncTransformer +from gloe.async_transformer import AsyncTransformer from gloe._transformer_utils import catch_transformer_exception from gloe.base_transformer import BaseTransformer, Flow +from gloe.exceptions import TransformerRequiresMultiArgs from gloe._generic_types import ( AsyncNext2, @@ -20,7 +21,6 @@ __all__ = ["Transformer"] -_I = TypeVar("_I", contravariant=True) _O = TypeVar("_O", covariant=True) Tr: TypeAlias = "Transformer" @@ -36,6 +36,7 @@ To = TypeVar("To", bound=BaseTransformer) Args = TypeVarTuple("Args") +NextArgs = TypeVarTuple("NextArgs") def _execute_flow(flow: Flow, arg: Any) -> Any: @@ -48,7 +49,7 @@ def _execute_flow(flow: Flow, arg: Any) -> Any: return result -class Transformer(BaseTransformer[_I, _O], ABC): +class Transformer(BaseTransformer[Any, _O], Generic[Unpack[Args], _O], ABC): """ A Transformer is the generic block with the responsibility to take an input of type `T` and transform it to an output of type `S`. @@ -67,9 +68,15 @@ class Stringifier(Transformer[dict, str]): def __init__(self): super().__init__() self.__class__.__annotations__ = self.transform.__annotations__ + orig_bases = getattr(self, "__orig_bases__", []) + transformer_args = [ + get_args(base) for base in orig_bases if get_origin(base) == Transformer + ] + if transformer_args and len(transformer_args[0]) > 2: + self._multi_args = True @abstractmethod - def transform(self, data: _I) -> _O: + def transform(self, data: Any) -> _O: """ Main method to be implemented and responsible to perform the transformer logic """ @@ -91,7 +98,7 @@ def __repr__(self): f" -> {self.output_annotation}" ) - def _safe_transform(self, data: _I) -> _O: + def _safe_transform(self, data: Any) -> _O: transform_exception = None transformed: Optional[_O] = None @@ -113,178 +120,90 @@ def __call__(self: "Transformer[None, _O]") -> _O: pass @overload - def __call__(self, data: _I) -> _O: - pass - - def __call__(self, data=None): - return _execute_flow(self._flow, data) - - @overload - def __rshift__(self, next_node: "Transformer[_O, O1]") -> "Transformer[_I, O1]": - pass - - @overload - def __rshift__( - self, - next_node: tuple["Tr[_O, O1]", "Tr[_O, O2]"], - ) -> "Transformer[_I, tuple[O1, O2]]": - pass - - @overload - def __rshift__( - self, - next_node: tuple["Tr[_O, O1]", "Tr[_O, O2]", "Tr[_O, O3]"], - ) -> "Transformer[_I, tuple[O1, O2, O3]]": - pass - - @overload - def __rshift__( - self, - next_node: tuple["Tr[_O, O1]", "Tr[_O, O2]", "Tr[_O, O3]", "Tr[_O, O4]"], - ) -> "Transformer[_I, tuple[O1, O2, O3, O4]]": - pass - - @overload - def __rshift__( - self, - next_node: tuple[ - "Tr[_O, O1]", "Tr[_O, O2]", "Tr[_O, O3]", "Tr[_O, O4]", "Tr[_O, O5]" - ], - ) -> "Transformer[_I, tuple[O1, O2, O3, O4, O5]]": - pass - - @overload - def __rshift__( - self, - next_node: tuple[ - "Tr[_O, O1]", - "Tr[_O, O2]", - "Tr[_O, O3]", - "Tr[_O, O4]", - "Tr[_O, O5]", - "Tr[_O, O6]", - ], - ) -> "Transformer[_I, tuple[O1, O2, O3, O4, O5, O6]]": - pass - - @overload - def __rshift__( - self, - next_node: tuple[ - "Tr[_O, O1]", - "Tr[_O, O2]", - "Tr[_O, O3]", - "Tr[_O, O4]", - "Tr[_O, O5]", - "Tr[_O, O6]", - "Tr[_O, O7]", - ], - ) -> "Transformer[_I, tuple[O1, O2, O3, O4, O5, O6, O7]]": + def __call__( + self: "Transformer[Unpack[Args], _O]", *data: Unpack[Args] + ) -> _O: pass - @overload - def __rshift__( - self, next_node: AsyncTransformer[_O, O1] - ) -> AsyncTransformer[_I, O1]: - pass + def __call__(self, *data): + if self._multi_args: + if len(data) == 1 and type(data[0]) is tuple: + data = data[0] + elif len(data) == 1: + raise TransformerRequiresMultiArgs() + return _execute_flow(self._flow, data) - @overload - def __rshift__( - self, next_node: AsyncNext2[_O, O1, O2] - ) -> AsyncTransformer[_I, tuple[O1, O2]]: - pass + if len(data) == 0: + return _execute_flow(self._flow, None) + if len(data) == 1: + return _execute_flow(self._flow, data[0]) + raise TypeError("Transformer expected a single argument") @overload def __rshift__( - self, next_node: AsyncNext3[_O, O1, O2, O3] - ) -> AsyncTransformer[_I, tuple[O1, O2, O3]]: + self: "Transformer[Unpack[Args], tuple[Unpack[NextArgs]]]", + next_node: "Transformer[Unpack[NextArgs], O1]", + ) -> "Transformer[Unpack[Args], O1]": pass @overload def __rshift__( - self, next_node: AsyncNext4[_O, O1, O2, O3, O4] - ) -> AsyncTransformer[_I, tuple[O1, O2, O3, O4]]: + self: "Transformer[Unpack[Args], tuple[Unpack[NextArgs]]]", + next_node: AsyncTransformer[Unpack[NextArgs], O1], + ) -> AsyncTransformer[Unpack[Args], O1]: pass @overload def __rshift__( - self, next_node: AsyncNext5[_O, O1, O2, O3, O4, O5] - ) -> AsyncTransformer[_I, tuple[O1, O2, O3, O4, O5]]: + self: "Transformer[Unpack[Args], list[Item]]", + next_node: "Transformer[Iterable[Item], O1]", + ) -> "Transformer[Unpack[Args], O1]": pass @overload def __rshift__( - self, next_node: AsyncNext6[_O, O1, O2, O3, O4, O5, O6] - ) -> AsyncTransformer[_I, tuple[O1, O2, O3, O4, O5, O6]]: + self: "Transformer[Unpack[Args], list[Item]]", + next_node: AsyncTransformer[Iterable[Item], O1], + ) -> AsyncTransformer[Unpack[Args], O1]: pass @overload - def __rshift__( - self, next_node: AsyncNext7[_O, O1, O2, O3, O4, O5, O6, O7] - ) -> AsyncTransformer[_I, tuple[O1, O2, O3, O4, O5, O6, O7]]: - pass - - def __rshift__(self, next_node): # pragma: no cover - pass - - -class MultiArgsTransformer( - Generic[Unpack[Args], _O], Transformer[tuple[Unpack[Args]], _O] -): - # The below ignored override errors are recommended by the documentation itself, - # "if you decide that type safety is not necessary", which is clearly the case. - # https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides - @override - def __call__( # type: ignore[override] - self: "MultiArgsTransformer[Unpack[Args], _O]", *data: Unpack[Args] - ) -> _O: - if len(data) == 1 and type(data[0]) is tuple: # type: ignore - data = data[0] # type: ignore - return _execute_flow(self._flow, data) - - @overload # type: ignore[override] - @override def __rshift__( self, next_node: "Transformer[_O, O1]" - ) -> "MultiArgsTransformer[Unpack[Args], O1]": + ) -> "Transformer[Unpack[Args], O1]": pass @overload - @override def __rshift__( self, next_node: tuple["Tr[_O, O1]", "Tr[_O, O2]"], - ) -> "MultiArgsTransformer[Unpack[Args], tuple[O1, O2]]": + ) -> "Transformer[Unpack[Args], tuple[O1, O2]]": pass @overload - @override def __rshift__( self, next_node: tuple["Tr[_O, O1]", "Tr[_O, O2]", "Tr[_O, O3]"], - ) -> "MultiArgsTransformer[Unpack[Args], tuple[O1, O2, O3]]": + ) -> "Transformer[Unpack[Args], tuple[O1, O2, O3]]": pass @overload - @override def __rshift__( self, next_node: tuple["Tr[_O, O1]", "Tr[_O, O2]", "Tr[_O, O3]", "Tr[_O, O4]"], - ) -> "MultiArgsTransformer[Unpack[Args], tuple[O1, O2, O3, O4]]": + ) -> "Transformer[Unpack[Args], tuple[O1, O2, O3, O4]]": pass @overload - @override def __rshift__( self, next_node: tuple[ "Tr[_O, O1]", "Tr[_O, O2]", "Tr[_O, O3]", "Tr[_O, O4]", "Tr[_O, O5]" ], - ) -> "MultiArgsTransformer[Unpack[Args], tuple[O1, O2, O3, O4, O5]]": + ) -> "Transformer[Unpack[Args], tuple[O1, O2, O3, O4, O5]]": pass @overload - @override def __rshift__( self, next_node: tuple[ @@ -295,11 +214,10 @@ def __rshift__( "Tr[_O, O5]", "Tr[_O, O6]", ], - ) -> "MultiArgsTransformer[Unpack[Args], tuple[O1, O2, O3, O4, O5, O6]]": + ) -> "Transformer[Unpack[Args], tuple[O1, O2, O3, O4, O5, O6]]": pass @overload - @override def __rshift__( self, next_node: tuple[ @@ -311,58 +229,50 @@ def __rshift__( "Tr[_O, O6]", "Tr[_O, O7]", ], - ) -> "MultiArgsTransformer[Unpack[Args], tuple[O1, O2, O3, O4, O5, O6, O7]]": + ) -> "Transformer[Unpack[Args], tuple[O1, O2, O3, O4, O5, O6, O7]]": pass @overload - @override def __rshift__( self, next_node: AsyncTransformer[_O, O1] - ) -> MultiArgsAsyncTransformer[_I, O1]: + ) -> AsyncTransformer[Unpack[Args], O1]: pass @overload - @override def __rshift__( self, next_node: AsyncNext2[_O, O1, O2] - ) -> MultiArgsAsyncTransformer[_I, tuple[O1, O2]]: + ) -> AsyncTransformer[Unpack[Args], tuple[O1, O2]]: pass @overload - @override def __rshift__( self, next_node: AsyncNext3[_O, O1, O2, O3] - ) -> MultiArgsAsyncTransformer[_I, tuple[O1, O2, O3]]: + ) -> AsyncTransformer[Unpack[Args], tuple[O1, O2, O3]]: pass @overload - @override def __rshift__( self, next_node: AsyncNext4[_O, O1, O2, O3, O4] - ) -> MultiArgsAsyncTransformer[_I, tuple[O1, O2, O3, O4]]: + ) -> AsyncTransformer[Unpack[Args], tuple[O1, O2, O3, O4]]: pass @overload - @override def __rshift__( self, next_node: AsyncNext5[_O, O1, O2, O3, O4, O5] - ) -> MultiArgsAsyncTransformer[_I, tuple[O1, O2, O3, O4, O5]]: + ) -> AsyncTransformer[Unpack[Args], tuple[O1, O2, O3, O4, O5]]: pass @overload - @override def __rshift__( self, next_node: AsyncNext6[_O, O1, O2, O3, O4, O5, O6] - ) -> MultiArgsAsyncTransformer[_I, tuple[O1, O2, O3, O4, O5, O6]]: + ) -> AsyncTransformer[Unpack[Args], tuple[O1, O2, O3, O4, O5, O6]]: pass @overload - @override def __rshift__( self, next_node: AsyncNext7[_O, O1, O2, O3, O4, O5, O6, O7] - ) -> MultiArgsAsyncTransformer[_I, tuple[O1, O2, O3, O4, O5, O6, O7]]: + ) -> AsyncTransformer[Unpack[Args], tuple[O1, O2, O3, O4, O5, O6, O7]]: pass - @override def __rshift__(self, next_node): # pragma: no cover pass diff --git a/pyproject.toml b/pyproject.toml index 5a93283f..6a642a8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,9 +34,14 @@ Documentation = "https://gloe.ideos.com.br" Issues = "https://github.com/ideos/gloe/issues" Repository = "https://github.com/ideos/gloe" +[dependency-groups] +dev = [ + 'mypy==1.19.1', + 'pytest==8.4.2', +] + [project.optional-dependencies] plot = ['pygraphviz>=1.11'] -types = ['mypy~=1.7.0'] [tool.pytest.ini_options] pythonpath = [ diff --git a/tests/multiargs/test_multiargs_transformer_types.py b/tests/multiargs/test_multiargs_transformer_types.py index 9b13bbae..a1738960 100644 --- a/tests/multiargs/test_multiargs_transformer_types.py +++ b/tests/multiargs/test_multiargs_transformer_types.py @@ -3,7 +3,7 @@ from gloe.utils import forward from typing_extensions import assert_type -from gloe import transformer, MultiArgsTransformer, Transformer +from gloe import transformer, Transformer from tests.lib.transformers import square from tests.type_utils.mypy_test_suite import MypyTestSuite @@ -21,13 +21,13 @@ def test_transformer_multiple_args(self): def sum2(num1: int, num2: float) -> float: return num1 + num2 - assert_type(sum2, MultiArgsTransformer[int, float, float]) + assert_type(sum2, Transformer[int, float, float]) @transformer def sum3(num1: int, num2: int, num3: int) -> int: return num1 + num2 + num3 - assert_type(sum3, MultiArgsTransformer[int, int, int, int]) + assert_type(sum3, Transformer[int, int, int, int]) def test_noargs_transformer(self): """ @@ -47,7 +47,7 @@ def sum2(num1: float, num2: float) -> float: pipeline = sum2 >> square - assert_type(pipeline, MultiArgsTransformer[float, float, float]) + assert_type(pipeline, Transformer[float, float, float]) pipeline2 = forward[float]() >> (square, square) >> sum2 @@ -55,4 +55,4 @@ def sum2(num1: float, num2: float) -> float: pipeline3 = sum2 >> (square, square) >> sum2 - assert_type(pipeline3, MultiArgsTransformer[float, float, float]) + assert_type(pipeline3, Transformer[float, float, float]) diff --git a/tests/partial/test_partial_transformer_types.py b/tests/partial/test_partial_transformer_types.py index 82d209ea..0b8a86d8 100644 --- a/tests/partial/test_partial_transformer_types.py +++ b/tests/partial/test_partial_transformer_types.py @@ -1,8 +1,9 @@ +from typing import reveal_type import asyncio import unittest from typing_extensions import assert_type -from gloe import partial_async_transformer, AsyncTransformer, Transformer +from gloe import partial_async_transformer, AsyncTransformer, Transformer, async_transformer, transformer from gloe.utils import forward from tests.lib.transformers import logarithm, repeat, format_currency from tests.type_utils.mypy_test_suite import MypyTestSuite diff --git a/tests/test_transformer_export.py b/tests/test_transformer_export.py index a9c7af1e..caab3bd6 100644 --- a/tests/test_transformer_export.py +++ b/tests/test_transformer_export.py @@ -6,6 +6,12 @@ from gloe.collection import Map from tests.lib.transformers import plus1, repeat_list +try: + import pygraphviz # noqa: F401 + _HAS_PYGRAPHVIZ = True +except ImportError: + _HAS_PYGRAPHVIZ = False + class TestTransformerExport(unittest.TestCase): foo = repeat_list(10) >> Map(plus1 >> repeat_list(10) >> Map(plus1)) @@ -16,6 +22,7 @@ def test_no_graphviz_installed(self, mock_import: MagicMock): with tempfile.NamedTemporaryFile(suffix=".dot", delete=False) as temp: self.foo.to_dot(temp.name) + @unittest.skipUnless(_HAS_PYGRAPHVIZ, "pygraphviz is required") def test_export_no_errors(self): with tempfile.NamedTemporaryFile(suffix=".dot", delete=False) as temp: self.foo.to_dot(temp.name, with_edge_labels=False) @@ -23,6 +30,7 @@ def test_export_no_errors(self): with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp: self.foo.to_image(temp.name) + @unittest.skipUnless(_HAS_PYGRAPHVIZ, "pygraphviz is required") def test_export_deprecated(self): with self.assertWarns(DeprecationWarning): import tempfile diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..80635f75 --- /dev/null +++ b/uv.lock @@ -0,0 +1,364 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "gloe" +source = { editable = "." } +dependencies = [ + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +plot = [ + { name = "pygraphviz", version = "1.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pygraphviz", version = "1.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "pygraphviz", marker = "extra == 'plot'", specifier = ">=1.11" }, + { name = "typing-extensions", specifier = "~=4.7" }, +] +provides-extras = ["plot"] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = "==1.19.1" }, + { name = "pytest", specifier = "==8.4.2" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "librt" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, + { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, + { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, + { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, + { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, + { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, + { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, + { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, + { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, + { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, + { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, + { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, + { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, + { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, + { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, + { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/2668bb01f568bc89ace53736df950845f8adfcacdf6da087d5cef12110cb/librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6", size = 56680, upload-time = "2026-01-14T12:56:02.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d4/dbb3edf2d0ec4ba08dcaf1865833d32737ad208962d4463c022cea6e9d3c/librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b", size = 58612, upload-time = "2026-01-14T12:56:03.616Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/64b029de4ac9901fcd47832c650a0fd050555a452bd455ce8deddddfbb9f/librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c", size = 163654, upload-time = "2026-01-14T12:56:04.975Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/95e2abb1b48eb8f8c7fc2ae945321a6b82777947eb544cc785c3f37165b2/librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5", size = 172477, upload-time = "2026-01-14T12:56:06.103Z" }, + { url = "https://files.pythonhosted.org/packages/7e/27/9bdf12e05b0eb089dd008d9c8aabc05748aad9d40458ade5e627c9538158/librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71", size = 186220, upload-time = "2026-01-14T12:56:09.958Z" }, + { url = "https://files.pythonhosted.org/packages/53/6a/c3774f4cc95e68ed444a39f2c8bd383fd18673db7d6b98cfa709f6634b93/librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e", size = 183841, upload-time = "2026-01-14T12:56:11.109Z" }, + { url = "https://files.pythonhosted.org/packages/58/6b/48702c61cf83e9c04ad5cec8cad7e5e22a2cde23a13db8ef341598897ddd/librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63", size = 179751, upload-time = "2026-01-14T12:56:12.278Z" }, + { url = "https://files.pythonhosted.org/packages/35/87/5f607fc73a131d4753f4db948833063c6aad18e18a4e6fbf64316c37ae65/librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94", size = 199319, upload-time = "2026-01-14T12:56:13.425Z" }, + { url = "https://files.pythonhosted.org/packages/6e/cc/b7c5ac28ae0f0645a9681248bae4ede665bba15d6f761c291853c5c5b78e/librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb", size = 43434, upload-time = "2026-01-14T12:56:14.781Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5d/dce0c92f786495adf2c1e6784d9c50a52fb7feb1cfb17af97a08281a6e82/librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be", size = 49801, upload-time = "2026-01-14T12:56:15.827Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, + { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, + { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pygraphviz" +version = "1.11" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/19/db/cc09516573e79a35ac73f437bdcf27893939923d1d06b439897ffc7f3217/pygraphviz-1.11.zip", hash = "sha256:a97eb5ced266f45053ebb1f2c6c6d29091690503e3a5c14be7f908b37b06f2d4", size = 120803, upload-time = "2023-06-01T16:23:45.932Z" } + +[[package]] +name = "pygraphviz" +version = "1.14" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/66/ca/823d5c74a73d6b8b08e1f5aea12468ef334f0732c65cbb18df2a7f285c87/pygraphviz-1.14.tar.gz", hash = "sha256:c10df02377f4e39b00ae17c862f4ee7e5767317f1c6b2dfd04cea6acc7fc2bea", size = 106003, upload-time = "2024-09-29T18:31:12.471Z" } + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] From 1deae9dc48d47941547618d8e979146b6c2051b0 Mon Sep 17 00:00:00 2001 From: Erick Lima Trentini Date: Sat, 24 Jan 2026 17:28:36 -0300 Subject: [PATCH 2/6] Refactor transformers to support unpacking of arguments and improve type safety - Updated AsyncTransformer and BaseTransformer to handle unpacked arguments using TypeVarTuple. - Enhanced the signature methods in transformers to correctly reflect the expected input types. - Modified the transform methods to accept variable arguments, improving flexibility. - Adjusted the handling of input data in the _ensure_base class to normalize input formats. - Improved type hints across various transformer classes and utility functions for better clarity and type checking. - Added tests to ensure that transformers correctly accept subclasses and handle multiple arguments. - Refactored the Map, MapAsync, MapOver, and MapOverAsync classes to support unpacked argument types. --- gloe/_composition_utils.py | 75 ++++++++++++----- gloe/async_transformer.py | 64 +++++++------- gloe/base_transformer.py | 83 ++++++++++++++----- gloe/collection/_map.py | 25 +++++- gloe/collection/_map_async.py | 26 +++++- gloe/collection/_mapover.py | 58 +++++++++++-- gloe/collection/_mapover_async.py | 60 ++++++++++++-- gloe/ensurer/_transformer_ensurer.py | 67 +++++++++------ gloe/functional.py | 65 ++++----------- gloe/gateways/_parallel.py | 2 +- gloe/transformers.py | 45 +++++----- tests/basic/test_basic_transformer_types.py | 20 +++++ tests/basic/test_transformer_basic.py | 17 ++++ tests/ensurer/test_transformer_ensurer.py | 4 +- .../test_async_multiargs_transformer.py | 4 +- .../test_multiargs_transformer_basic.py | 4 +- tests/test_transformer_graph.py | 16 ++-- tests/test_transformer_utils.py | 5 +- 18 files changed, 432 insertions(+), 208 deletions(-) diff --git a/gloe/_composition_utils.py b/gloe/_composition_utils.py index 7b759220..21fbc64c 100644 --- a/gloe/_composition_utils.py +++ b/gloe/_composition_utils.py @@ -20,23 +20,18 @@ def is_transformer(node): return isinstance(node, Transformer) -def _is_multi_args(transformer: BaseTransformer) -> bool: - return getattr(transformer, "_multi_args", False) - - def _resolve_serial_connection_signatures( transformer1: BaseTransformer, transformer2: BaseTransformer, generic_vars: dict ) -> Signature: - signature2 = transformer2.signature() - first_param = list(signature2.parameters.values())[0] - new_parameter = first_param.replace( - annotation=_specify_types(transformer1.input_type, generic_vars) - ) - new_signature = signature2.replace( - parameters=[new_parameter], + signature1 = transformer1.signature() + parameters = [ + parameter.replace(annotation=_specify_types(parameter.annotation, generic_vars)) + for parameter in signature1.parameters.values() + ] + return signature1.replace( + parameters=parameters, return_annotation=_specify_types(transformer2.output_type, generic_vars), ) - return new_signature def _compose_serial(transformer1, _transformer2): @@ -54,8 +49,15 @@ def _compose_serial(transformer1, _transformer2): generic_vars = {**input_generic_vars, **output_generic_vars} def transformer1_signature(_) -> Signature: + parameters = [ + parameter.replace( + annotation=_specify_types(parameter.annotation, generic_vars) + ) + for parameter in signature1.parameters.values() + ] return signature1.replace( - return_annotation=_specify_types(signature1.return_annotation, generic_vars) + parameters=parameters, + return_annotation=_specify_types(signature1.return_annotation, generic_vars), ) setattr( @@ -85,11 +87,10 @@ def __init__(self): super().__init__() self._flow = transformer1._flow + transformer2._flow - def transform(self, data): + def transform(self, *data): return None new_transformer = NewTransformer1() - new_transformer._multi_args = _is_multi_args(transformer1) else: class NewTransformer2(BaseNewTransformer, AsyncTransformer[_In, _NextOut]): @@ -97,11 +98,10 @@ def __init__(self): super().__init__() self._flow = transformer1._flow + transformer2._flow - async def transform_async(self, data): + async def transform_async(self, *data): return None new_transformer = NewTransformer2() - new_transformer._multi_args = _is_multi_args(transformer1) new_transformer.__class__.__name__ = transformer2.__class__.__name__ new_transformer._label = transformer2.label @@ -126,6 +126,41 @@ def _compose_diverging( ) class BaseNewTransformer: + def signature(self) -> Signature: + incident_signature = incident_transformer.signature() + generic_vars: dict = {} + for receiving_transformer in receiving_transformers: + generic_vars.update( + _match_types( + receiving_transformer.input_type, + incident_transformer.output_type, + ) + ) + generic_vars.update( + _match_types( + incident_transformer.output_type, + receiving_transformer.input_type, + ) + ) + + parameters = [ + parameter.replace( + annotation=_specify_types(parameter.annotation, generic_vars) + ) + for parameter in incident_signature.parameters.values() + ] + return_types = [ + _specify_types( + receiving_transformer.signature().return_annotation, + generic_vars, + ) + for receiving_transformer in receiving_transformers + ] + return incident_signature.replace( + parameters=parameters, + return_annotation=types.GenericAlias(tuple, tuple(return_types)), + ) + def __len__(self): lengths = [len(t) for t in receiving_transformers] return sum(lengths) + len(incident_transformer) @@ -141,11 +176,10 @@ def __init__(self): _Parallel(*receiving_transformers) ] - def transform(self, data): + def transform(self, *data): return None new_transformer = NewTransformer1() - new_transformer._multi_args = _is_multi_args(incident_transformer) else: @@ -158,11 +192,10 @@ def __init__(self): _ParallelAsync(*receiving_transformers) ] - async def transform_async(self, data): + async def transform_async(self, *data): return None new_transformer = NewTransformer2() - new_transformer._multi_args = _is_multi_args(incident_transformer) # new_transformer._previous = cast(Transformer, receiving_transformers) new_transformer.__class__.__name__ = "Converge" diff --git a/gloe/async_transformer.py b/gloe/async_transformer.py index 7521518d..8ba41d89 100644 --- a/gloe/async_transformer.py +++ b/gloe/async_transformer.py @@ -1,16 +1,16 @@ from abc import abstractmethod from inspect import Signature -from typing import TypeVar, overload, cast, Callable, Optional, Any, get_args, get_origin, TYPE_CHECKING +from typing import TypeVar, overload, cast, Callable, Optional, TYPE_CHECKING, Iterable -from typing_extensions import Self, Unpack, Generic, TypeVarTuple +from typing_extensions import Self, Unpack, Generic, TypeVarTuple, Never from gloe._plotting_utils import PlottingSettings, NodeType from gloe._transformer_utils import catch_transformer_exception from gloe.base_transformer import BaseTransformer, Flow -from gloe.exceptions import TransformerRequiresMultiArgs if TYPE_CHECKING: from gloe.transformers import Transformer + from gloe.utils import forward as Forward __all__ = ["AsyncTransformer"] @@ -23,12 +23,13 @@ _O5 = TypeVar("_O5") _O6 = TypeVar("_O6") _O7 = TypeVar("_O7") +Item = TypeVar("Item") Args = TypeVarTuple("Args") NextArgs = TypeVarTuple("NextArgs") -async def _execute_async_flow(flow: Flow, arg: Any) -> Any: +async def _execute_async_flow(flow: Flow, arg: object) -> object: result = arg for op in flow: if isinstance(op, BaseTransformer): @@ -44,7 +45,7 @@ async def _execute_async_flow(flow: Flow, arg: Any) -> Any: return result -class AsyncTransformer(Generic[Unpack[Args], _Out], BaseTransformer[Any, _Out]): +class AsyncTransformer(Generic[Unpack[Args], _Out], BaseTransformer[Unpack[Args], _Out]): def __init__(self): super().__init__() @@ -52,15 +53,9 @@ def __init__(self): node_type=NodeType.Transformer, is_async=True ) self.__class__.__annotations__ = self.transform_async.__annotations__ - orig_bases = getattr(self, "__orig_bases__", []) - transformer_args = [ - get_args(base) for base in orig_bases if get_origin(base) == AsyncTransformer - ] - if transformer_args and len(transformer_args[0]) > 2: - self._multi_args = True @abstractmethod - async def transform_async(self, data: Any) -> _Out: + async def transform_async(self, *data: Unpack[Args]) -> _Out: """ Method to perform the transformation asynchronously. @@ -89,12 +84,13 @@ def __repr__(self): f" -> {self.output_annotation}" ) - async def _safe_transform(self, data: Any) -> _Out: + async def _safe_transform(self, data: object) -> _Out: transform_exception = None transformed: Optional[_Out] = None try: - transformed = await self.transform_async(data) + args = cast(tuple[Unpack[Args]], self._unpack_call_args(data)) + transformed = await self.transform_async(*args) except Exception as exception: transform_exception = catch_transformer_exception(exception, self) @@ -108,7 +104,7 @@ async def _safe_transform(self, data: Any) -> _Out: def copy( self, - transform: Optional[Callable[[Self, Any], _Out]] = None, + transform: Optional[Callable[[Self, Unpack[Args]], _Out]] = None, regenerate_instance_id: bool = False, force: bool = False, ) -> Self: @@ -116,27 +112,17 @@ def copy( @overload async def __call__(self: "AsyncTransformer[None, _Out]") -> _Out: - return await _execute_async_flow(self._flow, None) + ... @overload async def __call__( self: "AsyncTransformer[Unpack[Args], _Out]", *data: Unpack[Args] ) -> _Out: - return await _execute_async_flow(self._flow, data) + ... async def __call__(self, *data): - if self._multi_args: - if len(data) == 1 and type(data[0]) is tuple: - data = data[0] - elif len(data) == 1: - raise TransformerRequiresMultiArgs() - return await _execute_async_flow(self._flow, data) - - if len(data) == 0: - return await _execute_async_flow(self._flow, None) - if len(data) == 1: - return await _execute_async_flow(self._flow, data[0]) - raise TypeError("Transformer expected a single argument") + packed = self._pack_call_args(data) + return cast(_Out, await _execute_async_flow(self._flow, packed)) @overload def __rshift__( @@ -152,6 +138,26 @@ def __rshift__( ) -> "AsyncTransformer[Unpack[Args], _NextOut]": pass + @overload + def __rshift__( + self: "AsyncTransformer[Unpack[Args], list[Item]]", + next_node: "Transformer[Iterable[Item], _NextOut]", + ) -> "AsyncTransformer[Unpack[Args], _NextOut]": + pass + + @overload + def __rshift__( + self: "AsyncTransformer[Unpack[Args], list[Item]]", + next_node: "AsyncTransformer[Iterable[Item], _NextOut]", + ) -> "AsyncTransformer[Unpack[Args], _NextOut]": + pass + + @overload + def __rshift__( + self, next_node: "Forward[Never]" + ) -> "AsyncTransformer[Unpack[Args], _Out]": + pass + @overload def __rshift__( self, next_node: "Transformer[_Out, _NextOut]" diff --git a/gloe/base_transformer.py b/gloe/base_transformer.py index b9552a4f..b202b185 100644 --- a/gloe/base_transformer.py +++ b/gloe/base_transformer.py @@ -21,11 +21,12 @@ cast, ) -from typing_extensions import Self, TypeAlias, deprecated +from typing_extensions import Self, TypeAlias, TypeVarTuple, Unpack, deprecated from gloe._gloe_graph import GloeGraph from gloe._plotting_utils import PlottingSettings, NodeType, dot_props -from gloe._typing_utils import _format_return_annotation +from gloe._typing_utils import _format_return_annotation, _specify_types +from gloe.exceptions import TransformerRequiresMultiArgs __all__ = ["BaseTransformer", "TransformerException", "PreviousTransformer"] @@ -39,6 +40,7 @@ _Out6 = TypeVar("_Out6") _Out7 = TypeVar("_Out7") +Args = TypeVarTuple("Args") PreviousTransformer: TypeAlias = Union[ None, @@ -53,6 +55,8 @@ TransformerChildren: TypeAlias = list["BaseTransformer"] +_NO_ARGS = object() + class TransformerException(Exception): def __init__( @@ -87,23 +91,21 @@ def from_transformer(transformer: "BaseTransformer") -> "GloeNode": output_annotation=transformer.output_annotation, ) - -_In = TypeVar("_In", contravariant=True) _Out = TypeVar("_Out", covariant=True) Flow = list["BaseTransformer"] -class BaseTransformer(Generic[_In, _Out], ABC): +class BaseTransformer(Generic[Unpack[Args], _Out], ABC): def __init__(self): self._children: TransformerChildren = [] self.id = uuid.uuid4() self.instance_id = uuid.uuid4() self.is_atomic = False - self._multi_args = False self._label = self.__class__.__name__ self._already_copied = False + self._signature_override: Optional[Signature] = None self._plotting_settings: PlottingSettings = PlottingSettings( invisible=False, node_type=NodeType.Transformer, @@ -148,7 +150,7 @@ def __eq__(self, other): def _copy( self: Self, - transform: Optional[Callable[[Self, _In], _Out]] = None, + transform: Optional[Callable[[Self, Unpack[Args]], _Out]] = None, regenerate_instance_id: bool = False, transform_method: str = "transform", force: bool = False, @@ -157,6 +159,7 @@ def _copy( copied._already_copied = True if transform is not None: + copied._signature_override = self.signature() setattr(copied, transform_method, types.MethodType(transform, copied)) old_instance_id = self.instance_id @@ -190,7 +193,7 @@ def _copy( def copy( self: Self, - transform: Optional[Callable[[Self, _In], _Out]] = None, + transform: Optional[Callable[[Self, Unpack[Args]], _Out]] = None, regenerate_instance_id: bool = False, force: bool = False, ) -> Self: @@ -201,6 +204,9 @@ def signature(self) -> Signature: """Transformer function-like signature""" def _signature(self, klass: Type, transform_method: str = "transform") -> Signature: + if self._signature_override is not None: + return self._signature_override + orig_bases = getattr(self, "__orig_bases__", []) transformer_args = [ get_args(base) for base in orig_bases if get_origin(base) == klass @@ -226,22 +232,52 @@ def _signature(self, klass: Type, transform_method: str = "transform") -> Signat } signature = inspect.signature(getattr(self, transform_method)) - new_return_annotation = specific_args.get( - signature.return_annotation, signature.return_annotation - ) - parameters = list(signature.parameters.values()) - if len(parameters) > 0: - parameter = parameters[0] - parameter = parameter.replace( - annotation=specific_args.get(parameter.annotation, parameter.annotation) + parameters = [ + parameter.replace( + annotation=_specify_types(parameter.annotation, specific_args) ) - parameters = [parameter] + for parameter in signature.parameters.values() + ] return signature.replace( - return_annotation=new_return_annotation, + return_annotation=_specify_types(signature.return_annotation, specific_args), parameters=parameters, ) + def _pack_call_args(self, args: tuple[object, ...]) -> object: + try: + param_count = len(self.signature().parameters) + except Exception: + param_count = 1 + if param_count <= 1: + if len(args) == 0: + return _NO_ARGS + if len(args) == 1: + return args[0] + raise TypeError("Transformer expected a single argument") + if len(args) == 0: + return () + if len(args) == 1 and isinstance(args[0], tuple): + return args[0] + if len(args) == 1: + raise TransformerRequiresMultiArgs() + return args + + def _unpack_call_args(self, data: object) -> tuple[object, ...]: + try: + param_count = len(self.signature().parameters) + except Exception: + param_count = 1 + if param_count == 0: + return () + if data is _NO_ARGS: + raise TypeError("Transformer expected a single argument") + if param_count == 1: + return (data,) + if isinstance(data, tuple): + return data + raise TransformerRequiresMultiArgs() + @property def output_type(self) -> Any: signature = self.signature() @@ -256,10 +292,13 @@ def output_annotation(self) -> str: @property def input_type(self) -> Any: - parameters = list(self.signature().parameters.items()) - if len(parameters) > 0: - parameter_type = parameters[0][1].annotation - return parameter_type + parameters = list(self.signature().parameters.values()) + if len(parameters) == 0: + return None + annotations = [parameter.annotation for parameter in parameters] + if len(annotations) == 1: + return annotations[0] + return types.GenericAlias(tuple, tuple(annotations)) @property def input_annotation(self) -> str: diff --git a/gloe/collection/_map.py b/gloe/collection/_map.py index d9cac8b7..21c7019f 100644 --- a/gloe/collection/_map.py +++ b/gloe/collection/_map.py @@ -1,9 +1,12 @@ -from typing import Generic, TypeVar, Iterable +from typing import Generic, TypeVar, Iterable, overload, cast + +from typing_extensions import TypeVarTuple, Unpack from gloe.transformers import Transformer _T = TypeVar("_T", contravariant=True) _U = TypeVar("_U", covariant=True) +Args = TypeVarTuple("Args") class Map(Generic[_T, _U], Transformer[Iterable[_T], list[_U]]): @@ -27,9 +30,25 @@ def get_user_posts(user: User) -> list[Post]: ... input iterable the yield the mapped item of the output iterable. """ - def __init__(self, mapping_transformer: Transformer[_T, _U]): + @overload + def __init__( + self: "Map[_T, _U]", mapping_transformer: Transformer[_T, _U] + ) -> None: + pass + + @overload + def __init__( + self: "Map[tuple[Unpack[Args]], _U]", + mapping_transformer: Transformer[Unpack[Args], _U], + ) -> None: + pass + + def __init__( + self, + mapping_transformer: Transformer[_T, _U] | Transformer[Unpack[Args], _U], + ): super().__init__() - self.mapping_transformer = mapping_transformer + self.mapping_transformer = cast(Transformer[_T, _U], mapping_transformer) self.plotting_settings.has_children = True self._children = [mapping_transformer] diff --git a/gloe/collection/_map_async.py b/gloe/collection/_map_async.py index b52e0301..d70862f6 100644 --- a/gloe/collection/_map_async.py +++ b/gloe/collection/_map_async.py @@ -1,9 +1,12 @@ -from typing import Generic, TypeVar, Iterable +from typing import Generic, TypeVar, Iterable, overload, cast + +from typing_extensions import TypeVarTuple, Unpack from gloe import AsyncTransformer _T = TypeVar("_T", contravariant=True) _U = TypeVar("_U", covariant=True) +Args = TypeVarTuple("Args") class MapAsync(Generic[_T, _U], AsyncTransformer[Iterable[_T], list[_U]]): @@ -27,9 +30,26 @@ async def get_user_posts(user: User) -> list[Post]: ... input iterable the yield the mapped item of the output iterable. """ - def __init__(self, mapping_transformer: AsyncTransformer[_T, _U]): + @overload + def __init__( + self: "MapAsync[_T, _U]", mapping_transformer: AsyncTransformer[_T, _U] + ) -> None: + pass + + @overload + def __init__( + self: "MapAsync[tuple[Unpack[Args]], _U]", + mapping_transformer: AsyncTransformer[Unpack[Args], _U], + ) -> None: + pass + + def __init__( + self, + mapping_transformer: AsyncTransformer[_T, _U] + | AsyncTransformer[Unpack[Args], _U], + ): super().__init__() - self.mapping_transformer = mapping_transformer + self.mapping_transformer = cast(AsyncTransformer[_T, _U], mapping_transformer) self.plotting_settings.has_children = True self._children = [mapping_transformer] diff --git a/gloe/collection/_mapover.py b/gloe/collection/_mapover.py index bf2456f9..3fe93f53 100644 --- a/gloe/collection/_mapover.py +++ b/gloe/collection/_mapover.py @@ -1,27 +1,73 @@ -from typing import Generic, Iterable, TypeVar +from inspect import Parameter, Signature +from typing import Generic, Iterable, TypeVar, overload, cast, get_args, get_origin +from typing_extensions import TypeVarTuple, Unpack from gloe.transformers import Transformer +Args = TypeVarTuple("Args") +_Head = TypeVar("_Head") +_Tail = TypeVarTuple("_Tail") _T = TypeVar("_T") _S = TypeVar("_S") _U = TypeVar("_U") -class MapOver(Generic[_T, _U], Transformer[_T, list[_U]]): +class MapOver(Generic[Unpack[Args], _U], Transformer[Unpack[Args], list[_U]]): + @overload def __init__( - self, + self: "MapOver[_T, _U]", iterable: Iterable[_S], mapping_transformer: Transformer[tuple[_T, _S], _U], + ) -> None: + pass + + @overload + def __init__( + self: "MapOver[_Head, Unpack[_Tail], _U]", + iterable: Iterable[_S], + mapping_transformer: Transformer[_Head, Unpack[_Tail], _S, _U], + ) -> None: + pass + + def __init__( + self, + iterable: Iterable[_S], + mapping_transformer: Transformer[tuple[_T, _S], _U] + | Transformer[_Head, Unpack[_Tail], _S, _U], ): super().__init__() self.iterable = iterable - self.mapping_transformer = mapping_transformer + self.mapping_transformer = cast( + Transformer[tuple[object, ...], _U], mapping_transformer + ) self.plotting_settings.has_children = True self._children = [mapping_transformer] - def transform(self, data: _T) -> list[_U]: + def signature(self) -> Signature: + mapping_signature = self.mapping_transformer.signature() + params = list(mapping_signature.parameters.values()) + return_annotation = self._signature(Transformer).return_annotation + + if len(params) == 0: + return Signature(parameters=[], return_annotation=return_annotation) + + if len(params) == 1: + annotation = params[0].annotation + origin = get_origin(annotation) + args = get_args(annotation) + if origin is tuple and len(args) > 0: + annotation = args[0] + param = params[0].replace( + annotation=annotation, + kind=Parameter.POSITIONAL_OR_KEYWORD, + ) + return Signature(parameters=[param], return_annotation=return_annotation) + + return Signature(parameters=params[:-1], return_annotation=return_annotation) + + def transform(self, *data: Unpack[Args]) -> list[_U]: lopping_result = [] for item in self.iterable: - lopping_result.append(self.mapping_transformer((data, item))) + lopping_result.append(self.mapping_transformer((*data, item))) return lopping_result diff --git a/gloe/collection/_mapover_async.py b/gloe/collection/_mapover_async.py index 48a05d81..9161b2cd 100644 --- a/gloe/collection/_mapover_async.py +++ b/gloe/collection/_mapover_async.py @@ -1,27 +1,75 @@ -from typing import Generic, Iterable, TypeVar +from inspect import Parameter, Signature +from typing import Generic, Iterable, TypeVar, overload, cast, get_args, get_origin +from typing_extensions import TypeVarTuple, Unpack from gloe import AsyncTransformer +Args = TypeVarTuple("Args") +_Head = TypeVar("_Head") +_Tail = TypeVarTuple("_Tail") _T = TypeVar("_T") _S = TypeVar("_S") _U = TypeVar("_U") -class MapOverAsync(Generic[_T, _U], AsyncTransformer[_T, list[_U]]): +class MapOverAsync(Generic[Unpack[Args], _U], AsyncTransformer[Unpack[Args], list[_U]]): + @overload def __init__( - self, + self: "MapOverAsync[_T, _U]", iterable: Iterable[_S], mapping_transformer: AsyncTransformer[tuple[_T, _S], _U], + ) -> None: + pass + + @overload + def __init__( + self: "MapOverAsync[_Head, Unpack[_Tail], _U]", + iterable: Iterable[_S], + mapping_transformer: AsyncTransformer[_Head, Unpack[_Tail], _S, _U], + ) -> None: + pass + + def __init__( + self, + iterable: Iterable[_S], + mapping_transformer: AsyncTransformer[tuple[_T, _S], _U] + | AsyncTransformer[_Head, Unpack[_Tail], _S, _U], ): super().__init__() self.iterable = iterable - self.mapping_transformer = mapping_transformer + self.mapping_transformer = cast( + AsyncTransformer[tuple[object, ...], _U], mapping_transformer + ) self.plotting_settings.has_children = True self._children = [mapping_transformer] - async def transform_async(self, data: _T) -> list[_U]: + def signature(self) -> Signature: + mapping_signature = self.mapping_transformer.signature() + params = list(mapping_signature.parameters.values()) + return_annotation = self._signature( + AsyncTransformer, "transform_async" + ).return_annotation + + if len(params) == 0: + return Signature(parameters=[], return_annotation=return_annotation) + + if len(params) == 1: + annotation = params[0].annotation + origin = get_origin(annotation) + args = get_args(annotation) + if origin is tuple and len(args) > 0: + annotation = args[0] + param = params[0].replace( + annotation=annotation, + kind=Parameter.POSITIONAL_OR_KEYWORD, + ) + return Signature(parameters=[param], return_annotation=return_annotation) + + return Signature(parameters=params[:-1], return_annotation=return_annotation) + + async def transform_async(self, *data: Unpack[Args]) -> list[_U]: lopping_result = [] for item in self.iterable: - result = await self.mapping_transformer((data, item)) + result = await self.mapping_transformer((*data, item)) lopping_result.append(result) return lopping_result diff --git a/gloe/ensurer/_transformer_ensurer.py b/gloe/ensurer/_transformer_ensurer.py index ec568771..0e602dcb 100644 --- a/gloe/ensurer/_transformer_ensurer.py +++ b/gloe/ensurer/_transformer_ensurer.py @@ -1,7 +1,7 @@ import inspect from abc import abstractmethod, ABC from types import FunctionType -from typing import Any, Callable, Generic, Sequence, TypeVar, cast, overload +from typing import Any, Callable, Generic, Optional, Sequence, TypeVar, cast, overload from typing_extensions import ParamSpec @@ -72,6 +72,14 @@ def __init__(self): self.output_ensurers_instances = [] self._input_data = None + @staticmethod + def _normalize_input(data: tuple[object, ...]) -> Optional[object]: + if len(data) == 0: + return None + if len(data) == 1: + return data[0] + return data + @overload def __call__(self, transformer: Transformer[_U, _S]) -> Transformer[_U, _S]: pass @@ -122,12 +130,13 @@ def _generate_new_transformer(self, transformer: Transformer) -> Transformer: if isinstance(first_node, Transformer) and len(transformer) == 1: - def transform(_, data): + def transform(_, *data): + incoming = self._normalize_input(data) for ensurer in self.input_ensurers_instances: - ensurer.validate_input(data) - output = transformer.transform(data) + ensurer.validate_input(incoming) + output = transformer.transform(*data) for ensurer in self.output_ensurers_instances: - ensurer.validate_output(data, output) + ensurer.validate_output(incoming, output) return output return transformer.copy(transform) @@ -137,11 +146,12 @@ def transform(_, data): or len(self.output_ensurers_instances) > 0 ): - def transform(_, data): + def transform(_, *data): + incoming = self._normalize_input(data) for ensurer in self.input_ensurers_instances: - ensurer.validate_input(data) - output = first_node.transform(data) - self._input_data = data + ensurer.validate_input(incoming) + output = first_node.transform(*data) + self._input_data = incoming return output transformer._flow[0] = first_node.copy(transform) @@ -151,8 +161,8 @@ def transform(_, data): and len(self.output_ensurers_instances) > 0 ): - def transform(_, data): - output = last_node.transform(data) + def transform(_, *data): + output = last_node.transform(*data) for ensurer in self.output_ensurers_instances: ensurer.validate_output(self._input_data, output) return output @@ -170,12 +180,13 @@ def _generate_new_async_transformer( if isinstance(first_node, AsyncTransformer) and len(_flow) == 1: - async def transform_async(_, data): + async def transform_async(_, *data): + incoming = self._normalize_input(data) for ensurer in self.input_ensurers_instances: - ensurer.validate_input(data) - output = await transformer.transform_async(data) + ensurer.validate_input(incoming) + output = await transformer.transform_async(*data) for ensurer in self.output_ensurers_instances: - ensurer.validate_output(data, output) + ensurer.validate_output(incoming, output) return output return transformer.copy(transform_async) @@ -186,21 +197,23 @@ async def transform_async(_, data): ): if isinstance(first_node, AsyncTransformer): - async def transform_async(_, data): + async def transform_async(_, *data): + incoming = self._normalize_input(data) for ensurer in self.input_ensurers_instances: - ensurer.validate_input(data) - output = await first_node.transform_async(data) - self._input_data = data + ensurer.validate_input(incoming) + output = await first_node.transform_async(*data) + self._input_data = incoming return output transformer._flow[0] = first_node.copy(transform_async) elif isinstance(first_node, Transformer): - def transform(_, data): + def transform(_, *data): + incoming = self._normalize_input(data) for ensurer in self.input_ensurers_instances: - ensurer.validate_input(data) - output = first_node.transform(data) - self._input_data = data + ensurer.validate_input(incoming) + output = first_node.transform(*data) + self._input_data = incoming return output transformer._flow[0] = first_node.copy(transform) @@ -208,8 +221,8 @@ def transform(_, data): if len(self.output_ensurers_instances) > 0: if isinstance(last_node, AsyncTransformer): - async def transform_async(_, data): - output = await last_node.transform_async(data) + async def transform_async(_, *data): + output = await last_node.transform_async(*data) for ensurer in self.output_ensurers_instances: ensurer.validate_output(self._input_data, output) return output @@ -218,8 +231,8 @@ async def transform_async(_, data): elif isinstance(last_node, Transformer): - def transform(_, data): - output = last_node.transform(data) + def transform(_, *data): + output = last_node.transform(*data) for ensurer in self.output_ensurers_instances: ensurer.validate_output(self._input_data, output) return output diff --git a/gloe/functional.py b/gloe/functional.py index 21d40aad..4b8090e3 100644 --- a/gloe/functional.py +++ b/gloe/functional.py @@ -6,7 +6,6 @@ from typing_extensions import Concatenate, TypeVarTuple, Unpack, ParamSpec from gloe.async_transformer import AsyncTransformer -from gloe.exceptions import TransformerRequiresMultiArgs from gloe.transformers import Transformer __all__ = [ @@ -65,13 +64,17 @@ def enrich_data(data: Data, enrichment_type: str) -> Data: def partial(*args: P1.args, **kwargs: P1.kwargs) -> Transformer[A, S]: func_signature = inspect.signature(func) + parameters = list(func_signature.parameters.values()) + partial_signature = func_signature.replace( + parameters=parameters[:1], + ) class LambdaTransformer(Transformer[A, S]): __doc__ = func.__doc__ __annotations__ = cast(FunctionType, func).__annotations__ def signature(self) -> Signature: - return func_signature + return partial_signature def transform(self, data: A) -> S: return func(data, *args, **kwargs) @@ -128,13 +131,17 @@ async def load_data(user_id: int, data_type: str) -> Data: def partial(*args: P1.args, **kwargs: P1.kwargs) -> AsyncTransformer[A, S]: func_signature = inspect.signature(func) + parameters = list(func_signature.parameters.values()) + partial_signature = func_signature.replace( + parameters=parameters[:1], + ) class LambdaTransformer(AsyncTransformer[A, S]): __doc__ = func.__doc__ __annotations__ = cast(FunctionType, func).__annotations__ def signature(self) -> Signature: - return func_signature + return partial_signature async def transform_async(self, data: A) -> S: return await func(data, *args, **kwargs) @@ -192,28 +199,6 @@ def filter_subscribed_users(users: list[User]) -> list[User]: """ func_signature = inspect.signature(func) - if len(func_signature.parameters) > 1: - - class LambdaMultiArgsTransformer(Transformer): - __doc__ = func.__doc__ - __annotations__ = cast(FunctionType, func).__annotations__ - - def signature(self) -> Signature: - return func_signature - - def transform(self, data): - if type(data) is tuple: - if len(data) == 1: - raise TransformerRequiresMultiArgs() - return func(*data) - raise NotImplementedError() # pragma: no cover - - lambda_transformer1 = LambdaMultiArgsTransformer() - lambda_transformer1._multi_args = True - lambda_transformer1.__class__.__name__ = func.__name__ - lambda_transformer1._label = func.__name__ - return cast(Transformer, lambda_transformer1) - class LambdaTransformer(Transformer): __doc__ = func.__doc__ __annotations__ = cast(FunctionType, func).__annotations__ @@ -221,10 +206,10 @@ class LambdaTransformer(Transformer): def signature(self) -> Signature: return func_signature - def transform(self, data=None): + def transform(self, *data): if len(func_signature.parameters) == 0: return func() - return func(data) + return func(*data) lambda_transformer2 = LambdaTransformer() lambda_transformer2.__class__.__name__ = func.__name__ @@ -273,28 +258,6 @@ async def get_user_by_role(role: str) -> list[User]: """ func_signature = inspect.signature(func) - if len(func_signature.parameters) > 1: - - class LambdaMultiArgsTransformer(AsyncTransformer): - __doc__ = func.__doc__ - __annotations__ = cast(FunctionType, func).__annotations__ - - def signature(self) -> Signature: - return func_signature - - async def transform_async(self, data): - if type(data) is tuple: - if len(data) == 1: - raise TransformerRequiresMultiArgs() - return await func(*data) - raise NotImplementedError() # pragma: no cover - - lambda_transformer1 = LambdaMultiArgsTransformer() - lambda_transformer1._multi_args = True - lambda_transformer1.__class__.__name__ = func.__name__ - lambda_transformer1._label = func.__name__ - return cast(AsyncTransformer, lambda_transformer1) - class LambdaAsyncTransformer(AsyncTransformer): __doc__ = func.__doc__ __annotations__ = cast(FunctionType, func).__annotations__ @@ -302,10 +265,10 @@ class LambdaAsyncTransformer(AsyncTransformer): def signature(self) -> Signature: return func_signature - async def transform_async(self, data): + async def transform_async(self, *data): if len(func_signature.parameters) == 0: return await func() - return await func(data) + return await func(*data) lambda_transformer = LambdaAsyncTransformer() lambda_transformer.__class__.__name__ = func.__name__ diff --git a/gloe/gateways/_parallel.py b/gloe/gateways/_parallel.py index f390bd4d..3ef54a91 100644 --- a/gloe/gateways/_parallel.py +++ b/gloe/gateways/_parallel.py @@ -28,7 +28,7 @@ def transform(self, data: _In) -> tuple[Any, ...]: class _ParallelAsync(_base_gateway[_In], AsyncTransformer[_In, tuple[Any, ...]]): async def transform_async(self, data: _In) -> tuple[Any, ...]: - results = [None] * len(self._children) + results: list[object] = [None] * len(self._children) indexed_children = list(enumerate(self._children)) async_children = [ diff --git a/gloe/transformers.py b/gloe/transformers.py index 1c075273..9e4dbcd0 100644 --- a/gloe/transformers.py +++ b/gloe/transformers.py @@ -1,14 +1,13 @@ from abc import ABC, abstractmethod from inspect import Signature -from typing import TypeVar, overload, cast, Optional, Any, get_args, get_origin, Iterable +from typing import TypeVar, overload, cast, Optional, Iterable, TYPE_CHECKING -from typing_extensions import Generic, TypeAlias, Unpack, TypeVarTuple +from typing_extensions import Generic, TypeAlias, Unpack, TypeVarTuple, Never from gloe.async_transformer import AsyncTransformer from gloe._transformer_utils import catch_transformer_exception from gloe.base_transformer import BaseTransformer, Flow -from gloe.exceptions import TransformerRequiresMultiArgs from gloe._generic_types import ( AsyncNext2, @@ -19,6 +18,9 @@ AsyncNext7, ) +if TYPE_CHECKING: + from gloe.utils import forward as Forward + __all__ = ["Transformer"] _O = TypeVar("_O", covariant=True) @@ -39,7 +41,7 @@ NextArgs = TypeVarTuple("NextArgs") -def _execute_flow(flow: Flow, arg: Any) -> Any: +def _execute_flow(flow: Flow, arg: object) -> object: result = arg for op in flow: if isinstance(op, Transformer): @@ -49,7 +51,7 @@ def _execute_flow(flow: Flow, arg: Any) -> Any: return result -class Transformer(BaseTransformer[Any, _O], Generic[Unpack[Args], _O], ABC): +class Transformer(BaseTransformer[Unpack[Args], _O], Generic[Unpack[Args], _O], ABC): """ A Transformer is the generic block with the responsibility to take an input of type `T` and transform it to an output of type `S`. @@ -68,15 +70,9 @@ class Stringifier(Transformer[dict, str]): def __init__(self): super().__init__() self.__class__.__annotations__ = self.transform.__annotations__ - orig_bases = getattr(self, "__orig_bases__", []) - transformer_args = [ - get_args(base) for base in orig_bases if get_origin(base) == Transformer - ] - if transformer_args and len(transformer_args[0]) > 2: - self._multi_args = True @abstractmethod - def transform(self, data: Any) -> _O: + def transform(self, *data: Unpack[Args]) -> _O: """ Main method to be implemented and responsible to perform the transformer logic """ @@ -98,12 +94,13 @@ def __repr__(self): f" -> {self.output_annotation}" ) - def _safe_transform(self, data: Any) -> _O: + def _safe_transform(self, data: object) -> _O: transform_exception = None transformed: Optional[_O] = None try: - transformed = self.transform(data) + args = cast(tuple[Unpack[Args]], self._unpack_call_args(data)) + transformed = self.transform(*args) except Exception as exception: transform_exception = catch_transformer_exception(exception, self) @@ -126,18 +123,8 @@ def __call__( pass def __call__(self, *data): - if self._multi_args: - if len(data) == 1 and type(data[0]) is tuple: - data = data[0] - elif len(data) == 1: - raise TransformerRequiresMultiArgs() - return _execute_flow(self._flow, data) - - if len(data) == 0: - return _execute_flow(self._flow, None) - if len(data) == 1: - return _execute_flow(self._flow, data[0]) - raise TypeError("Transformer expected a single argument") + packed = self._pack_call_args(data) + return cast(_O, _execute_flow(self._flow, packed)) @overload def __rshift__( @@ -167,6 +154,12 @@ def __rshift__( ) -> AsyncTransformer[Unpack[Args], O1]: pass + @overload + def __rshift__( + self, next_node: "Forward[Never]" + ) -> "Transformer[Unpack[Args], _O]": + pass + @overload def __rshift__( self, next_node: "Transformer[_O, O1]" diff --git a/tests/basic/test_basic_transformer_types.py b/tests/basic/test_basic_transformer_types.py index a03a12be..67ae352d 100644 --- a/tests/basic/test_basic_transformer_types.py +++ b/tests/basic/test_basic_transformer_types.py @@ -1,9 +1,11 @@ from typing import TypeVar +from abc import ABC, abstractmethod from typing_extensions import assert_type from gloe import ( Transformer, + transformer, async_transformer, AsyncTransformer, ) @@ -111,3 +113,21 @@ async def _square(num: int) -> float: assert_type(async_pipeline3, AsyncTransformer[int, tuple[float, str]]) assert_type(async_pipeline4, AsyncTransformer[int, tuple[str, float]]) assert_type(async_pipeline5, AsyncTransformer[int, str]) + + def test_abstract_input_accepts_subclass(self): + class Animal(ABC): + @abstractmethod + def sound(self) -> str: + raise NotImplementedError() + + class Dog(Animal): + def sound(self) -> str: + return "woof" + + @transformer + def speak(animal: Animal) -> str: + return animal.sound() + + dog = Dog() + speak(dog) + assert_type(speak, Transformer[Animal, str]) diff --git a/tests/basic/test_transformer_basic.py b/tests/basic/test_transformer_basic.py index 5c25b117..62fdab76 100644 --- a/tests/basic/test_transformer_basic.py +++ b/tests/basic/test_transformer_basic.py @@ -1,6 +1,7 @@ import asyncio import re import unittest +from abc import ABC, abstractmethod from typing import cast from gloe import ( @@ -49,6 +50,22 @@ def many_args(arg1: tuple[float, float], arg2: float) -> float: self.assertEqual(graph(3), 81 + 8 + 16.0) + def test_transformer_accepts_subclass_input(self): + class Animal(ABC): + @abstractmethod + def sound(self) -> str: + raise NotImplementedError() + + class Dog(Animal): + def sound(self) -> str: + return "woof" + + @transformer + def speak(animal: Animal) -> str: + return animal.sound() + + self.assertEqual(speak(Dog()), "woof") + def test_transformer_hash(self): self.assertEqual(hash(square.id), square.__hash__()) diff --git a/tests/ensurer/test_transformer_ensurer.py b/tests/ensurer/test_transformer_ensurer.py index b45ffc92..705b7da0 100644 --- a/tests/ensurer/test_transformer_ensurer.py +++ b/tests/ensurer/test_transformer_ensurer.py @@ -124,7 +124,7 @@ def multiply_by_odd_equals_even(n1: int, n2: int) -> int: def test_function_ensure(self): even_ensurer = ensure(incoming=[is_even]) - def build_divide_by_2_pipeline() -> Transformer[int, float]: + def build_divide_by_2_pipeline() -> Transformer[float, float]: return divide_by_2 ensured_is_even_pipeline = even_ensurer(build_divide_by_2_pipeline) @@ -196,7 +196,7 @@ def test_function_many_ensurers(self): incoming=[is_even], outcome=[is_greater_than_10] ) - def build_multiply_by_2_pipeline() -> Transformer[int, float]: + def build_multiply_by_2_pipeline() -> Transformer[float, float]: return times2 ensured_even_and_greater_than_10_pipeline = even_and_greater_than_10_ensurer( diff --git a/tests/multiargs/test_async_multiargs_transformer.py b/tests/multiargs/test_async_multiargs_transformer.py index 4f07454b..ad9ad6f9 100644 --- a/tests/multiargs/test_async_multiargs_transformer.py +++ b/tests/multiargs/test_async_multiargs_transformer.py @@ -41,12 +41,12 @@ async def concat(arg1: str, arg2: str) -> str: async def test_composition_transform_method(self): test1 = sum2 >> async_plus1 - result = await test1.transform_async((5, 3)) + result = await test1.transform_async(5, 3) self.assertIsNone(result) test2 = sum2 >> (async_plus1, async_plus1) - result2 = await test2.transform_async((5, 3)) + result2 = await test2.transform_async(5, 3) self.assertIsNone(result2) async def test_noargs_basic_call(self): diff --git a/tests/multiargs/test_multiargs_transformer_basic.py b/tests/multiargs/test_multiargs_transformer_basic.py index 26f4177f..7f6cbd5c 100644 --- a/tests/multiargs/test_multiargs_transformer_basic.py +++ b/tests/multiargs/test_multiargs_transformer_basic.py @@ -43,12 +43,12 @@ def test_diverging_composition(self): def test_composition_transform_method(self): test1 = sum2 >> plus1 - result = test1.transform((5, 3)) + result = test1.transform(5, 3) self.assertIsNone(result) test2 = sum2 >> (plus1, plus1) - result2 = test2.transform((5, 3)) + result2 = test2.transform(5, 3) self.assertIsNone(result2) def test_single_arg_exception(self): diff --git a/tests/test_transformer_graph.py b/tests/test_transformer_graph.py index d115a28d..95668f00 100644 --- a/tests/test_transformer_graph.py +++ b/tests/test_transformer_graph.py @@ -134,7 +134,7 @@ def test_complex_divergent_case(self): divergent = ( square >> ( - plus1 >> if_is_even.Then(square_root).Else(forward()), + plus1 >> if_is_even.Then(square_root).Else(forward[float]()), minus1 >> natural_logarithm, times2 >> divide_by_2, ) @@ -195,7 +195,9 @@ def aux_last(data: tuple[tuple[float, float], float]) -> float: self._assert_graph_has_edges(graph, expected_edges) def test_simple_conditional_case(self): - conditional = square_root >> if_is_even.Then(plus1).Else(forward()) >> minus1 + conditional = ( + square_root >> if_is_even.Then(plus1).Else(forward[float]()) >> minus1 + ) graph: GloeGraph = conditional.graph() @@ -214,7 +216,9 @@ def test_simple_conditional_case(self): def test_complex_conditional_case(self): then_graph = plus1 >> square >> (times2, divide_by_2) >> sum_tuple2 conditional = ( - square_root >> if_is_even.Then(then_graph).Else(forward()) >> minus1 + square_root + >> if_is_even.Then(then_graph).Else(forward[float]()) + >> minus1 ) graph: GloeGraph = conditional.graph() @@ -298,7 +302,9 @@ def test_partial_transformer_case(self): def test_nodes_properties_case(self): then_graph = plus1 >> square >> (times2, divide_by_2) >> sum_tuple2 conditional = ( - square_root >> if_is_even.Then(then_graph).Else(forward()) >> minus1 + square_root + >> if_is_even.Then(then_graph).Else(forward[float]()) + >> minus1 ) graph = conditional.graph() @@ -348,7 +354,7 @@ def test_edge_labels_case(self): self._assert_edge_properties(graph, expected_edge_props) def test_subgraphs(self): - nested_transformer = forward[list[int]]() >> Map(square >> square_root) + nested_transformer = forward[list[float]]() >> Map(square >> square_root) nested_graph = nested_transformer.graph() subgraphs = nested_graph.subgraphs self.assertEqual(len(subgraphs), 1) diff --git a/tests/test_transformer_utils.py b/tests/test_transformer_utils.py index c3ec3cd0..9436fdce 100644 --- a/tests/test_transformer_utils.py +++ b/tests/test_transformer_utils.py @@ -1,7 +1,7 @@ import unittest from typing import Union -from gloe import transformer +from gloe import transformer, Transformer from gloe.transformers import _execute_flow from gloe.utils import forward, forward_incoming, debug, attach from tests.lib.transformers import sum_tuple2, square_root, square @@ -42,7 +42,8 @@ def test_tuple(num: float) -> tuple[int, float]: ) def test_debug(self): - test_debug = forward[int]() >> debug() + base: Transformer[int, int] = forward[int]() + test_debug: Transformer[int, int] = base >> debug[int]() result = test_debug(5) self.assertEqual(result, 5) From 55211494d01558b9807a2cd6d3aa532697910cbf Mon Sep 17 00:00:00 2001 From: Erick Lima Trentini Date: Sat, 24 Jan 2026 19:59:45 -0300 Subject: [PATCH 3/6] refactor: clean up imports and improve type hints in transformers and async_transformer --- gloe/async_transformer.py | 15 +++++++++++---- gloe/base_transformer.py | 13 ++++++++++--- gloe/transformers.py | 10 ++++------ tests/test_transformer_utils.py | 5 ++--- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/gloe/async_transformer.py b/gloe/async_transformer.py index 8ba41d89..7b0de5d5 100644 --- a/gloe/async_transformer.py +++ b/gloe/async_transformer.py @@ -1,8 +1,8 @@ from abc import abstractmethod from inspect import Signature -from typing import TypeVar, overload, cast, Callable, Optional, TYPE_CHECKING, Iterable +from typing import TypeVar, overload, cast, Callable, Optional, Iterable, TYPE_CHECKING -from typing_extensions import Self, Unpack, Generic, TypeVarTuple, Never +from typing_extensions import Self, Unpack, Generic, TypeVarTuple from gloe._plotting_utils import PlottingSettings, NodeType from gloe._transformer_utils import catch_transformer_exception @@ -10,7 +10,6 @@ if TYPE_CHECKING: from gloe.transformers import Transformer - from gloe.utils import forward as Forward __all__ = ["AsyncTransformer"] @@ -154,7 +153,15 @@ def __rshift__( @overload def __rshift__( - self, next_node: "Forward[Never]" + self: "AsyncTransformer[Unpack[Args], _Out]", + next_node: "Transformer[_Out, _Out]", + ) -> "AsyncTransformer[Unpack[Args], _Out]": + pass + + @overload + def __rshift__( + self: "AsyncTransformer[Unpack[Args], _Out]", + next_node: "AsyncTransformer[_Out, _Out]", ) -> "AsyncTransformer[Unpack[Args], _Out]": pass diff --git a/gloe/base_transformer.py b/gloe/base_transformer.py index b202b185..52c7b8aa 100644 --- a/gloe/base_transformer.py +++ b/gloe/base_transformer.py @@ -208,9 +208,16 @@ def _signature(self, klass: Type, transform_method: str = "transform") -> Signat return self._signature_override orig_bases = getattr(self, "__orig_bases__", []) - transformer_args = [ - get_args(base) for base in orig_bases if get_origin(base) == klass - ] + transformer_args = [] + for base in orig_bases: + origin = get_origin(base) + if origin is None: + continue + try: + if issubclass(origin, klass): + transformer_args.append(get_args(base)) + except TypeError: + continue generic_args = [ get_args(base) for base in orig_bases if get_origin(base) == Generic ] diff --git a/gloe/transformers.py b/gloe/transformers.py index 9e4dbcd0..1c120309 100644 --- a/gloe/transformers.py +++ b/gloe/transformers.py @@ -1,9 +1,9 @@ from abc import ABC, abstractmethod from inspect import Signature -from typing import TypeVar, overload, cast, Optional, Iterable, TYPE_CHECKING +from typing import TypeVar, overload, cast, Optional, Iterable -from typing_extensions import Generic, TypeAlias, Unpack, TypeVarTuple, Never +from typing_extensions import Generic, TypeAlias, Unpack, TypeVarTuple from gloe.async_transformer import AsyncTransformer from gloe._transformer_utils import catch_transformer_exception @@ -18,9 +18,6 @@ AsyncNext7, ) -if TYPE_CHECKING: - from gloe.utils import forward as Forward - __all__ = ["Transformer"] _O = TypeVar("_O", covariant=True) @@ -156,7 +153,8 @@ def __rshift__( @overload def __rshift__( - self, next_node: "Forward[Never]" + self: "Transformer[Unpack[Args], _O]", + next_node: "Transformer[_O, _O]", ) -> "Transformer[Unpack[Args], _O]": pass diff --git a/tests/test_transformer_utils.py b/tests/test_transformer_utils.py index 9436fdce..c3ec3cd0 100644 --- a/tests/test_transformer_utils.py +++ b/tests/test_transformer_utils.py @@ -1,7 +1,7 @@ import unittest from typing import Union -from gloe import transformer, Transformer +from gloe import transformer from gloe.transformers import _execute_flow from gloe.utils import forward, forward_incoming, debug, attach from tests.lib.transformers import sum_tuple2, square_root, square @@ -42,8 +42,7 @@ def test_tuple(num: float) -> tuple[int, float]: ) def test_debug(self): - base: Transformer[int, int] = forward[int]() - test_debug: Transformer[int, int] = base >> debug[int]() + test_debug = forward[int]() >> debug() result = test_debug(5) self.assertEqual(result, 5) From 4a9d6632b043ae4e7593e41ca12ee94ea75830cd Mon Sep 17 00:00:00 2001 From: Erick Lima Trentini Date: Sat, 24 Jan 2026 23:59:36 -0300 Subject: [PATCH 4/6] refactor: enhance multi-argument transformer handling and type safety and add a few more tests --- gloe/async_transformer.py | 12 +++- gloe/base_transformer.py | 35 ++++++++++- gloe/collection/_mapover.py | 27 ++++++++ gloe/collection/_mapover_async.py | 27 ++++++++ gloe/functional.py | 4 +- gloe/transformers.py | 12 +++- .../collection/test_transformer_collection.py | 24 +++++++ .../test_transformer_collection_async.py | 19 ++++++ .../test_transformer_collection_types.py | 38 +++++++++++- .../test_multiargs_transformer_basic.py | 20 ++++++ .../test_multiargs_transformer_types.py | 2 +- tests/type_errors/test_type_errors.py | 62 +++++++++++++++++++ 12 files changed, 271 insertions(+), 11 deletions(-) create mode 100644 tests/type_errors/test_type_errors.py diff --git a/gloe/async_transformer.py b/gloe/async_transformer.py index 7b0de5d5..275ed1a8 100644 --- a/gloe/async_transformer.py +++ b/gloe/async_transformer.py @@ -6,7 +6,8 @@ from gloe._plotting_utils import PlottingSettings, NodeType from gloe._transformer_utils import catch_transformer_exception -from gloe.base_transformer import BaseTransformer, Flow +from gloe.base_transformer import BaseTransformer, Flow, _NO_ARGS +from gloe.exceptions import TransformerRequiresMultiArgs if TYPE_CHECKING: from gloe.transformers import Transformer @@ -89,6 +90,13 @@ async def _safe_transform(self, data: object) -> _Out: transformed: Optional[_Out] = None try: args = cast(tuple[Unpack[Args]], self._unpack_call_args(data)) + except TransformerRequiresMultiArgs as exception: + raise TypeError(self._param_mismatch_message(data)) from exception + except TypeError as exception: + if data is _NO_ARGS: + raise TypeError(self._param_mismatch_message(data)) from exception + raise + try: transformed = await self.transform_async(*args) except Exception as exception: transform_exception = catch_transformer_exception(exception, self) @@ -110,7 +118,7 @@ def copy( return self._copy(transform, regenerate_instance_id, "transform_async", force) @overload - async def __call__(self: "AsyncTransformer[None, _Out]") -> _Out: + async def __call__(self: "AsyncTransformer[_Out]") -> _Out: ... @overload diff --git a/gloe/base_transformer.py b/gloe/base_transformer.py index 52c7b8aa..4f0b6bf7 100644 --- a/gloe/base_transformer.py +++ b/gloe/base_transformer.py @@ -256,7 +256,11 @@ def _pack_call_args(self, args: tuple[object, ...]) -> object: param_count = len(self.signature().parameters) except Exception: param_count = 1 - if param_count <= 1: + if param_count == 0: + if len(args) == 0: + return _NO_ARGS + raise TypeError("Transformer expected no arguments") + if param_count == 1: if len(args) == 0: return _NO_ARGS if len(args) == 1: @@ -280,11 +284,40 @@ def _unpack_call_args(self, data: object) -> tuple[object, ...]: if data is _NO_ARGS: raise TypeError("Transformer expected a single argument") if param_count == 1: + try: + params = list(self.signature().parameters.values()) + except Exception: + return (data,) + annotation = params[0].annotation if params else None + origin = get_origin(annotation) + args = get_args(annotation) + if origin is tuple: + if not isinstance(data, tuple): + raise TypeError(self._param_mismatch_message(data)) + if args and args[-1] is not Ellipsis and len(args) != len(data): + raise TypeError(self._param_mismatch_message(data)) return (data,) if isinstance(data, tuple): return data raise TransformerRequiresMultiArgs() + def _param_mismatch_message(self, data: object) -> str: + try: + param_count = len(self.signature().parameters) + except Exception: + param_count = 1 + if data is _NO_ARGS: + got = "no arguments" + elif isinstance(data, tuple): + got = f"{len(data)} arguments" + else: + got = "1 argument" + expected = self.input_annotation + return ( + f"Transformer parameter mismatch for {type(self).__name__}: " + f"expected {param_count} arguments ({expected}), got {got}." + ) + @property def output_type(self) -> Any: signature = self.signature() diff --git a/gloe/collection/_mapover.py b/gloe/collection/_mapover.py index 3fe93f53..cd8af791 100644 --- a/gloe/collection/_mapover.py +++ b/gloe/collection/_mapover.py @@ -37,6 +37,7 @@ def __init__( | Transformer[_Head, Unpack[_Tail], _S, _U], ): super().__init__() + self._validate_mapping_transformer(mapping_transformer) self.iterable = iterable self.mapping_transformer = cast( Transformer[tuple[object, ...], _U], mapping_transformer @@ -44,6 +45,32 @@ def __init__( self.plotting_settings.has_children = True self._children = [mapping_transformer] + def _validate_mapping_transformer( + self, + mapping_transformer: Transformer[tuple[_T, _S], _U] + | Transformer[_Head, Unpack[_Tail], _S, _U], + ) -> None: + mapping_signature = mapping_transformer.signature() + params = list(mapping_signature.parameters.values()) + if len(params) == 0: + raise TypeError( + "MapOver mapping transformer must accept extra arguments plus the item." + ) + if len(params) == 1: + annotation = params[0].annotation + origin = get_origin(annotation) + args = get_args(annotation) + if origin is not tuple: + raise TypeError( + "MapOver mapping transformer must accept a tuple of (extra, item). " + "Use Map for single-argument mapping." + ) + if len(args) == 1 and args[0] is not ...: + raise TypeError( + "MapOver mapping transformer tuple must include the extra argument " + "and the item." + ) + def signature(self) -> Signature: mapping_signature = self.mapping_transformer.signature() params = list(mapping_signature.parameters.values()) diff --git a/gloe/collection/_mapover_async.py b/gloe/collection/_mapover_async.py index 9161b2cd..8afb2168 100644 --- a/gloe/collection/_mapover_async.py +++ b/gloe/collection/_mapover_async.py @@ -36,6 +36,7 @@ def __init__( | AsyncTransformer[_Head, Unpack[_Tail], _S, _U], ): super().__init__() + self._validate_mapping_transformer(mapping_transformer) self.iterable = iterable self.mapping_transformer = cast( AsyncTransformer[tuple[object, ...], _U], mapping_transformer @@ -67,6 +68,32 @@ def signature(self) -> Signature: return Signature(parameters=params[:-1], return_annotation=return_annotation) + def _validate_mapping_transformer( + self, + mapping_transformer: AsyncTransformer[tuple[_T, _S], _U] + | AsyncTransformer[_Head, Unpack[_Tail], _S, _U], + ) -> None: + mapping_signature = mapping_transformer.signature() + params = list(mapping_signature.parameters.values()) + if len(params) == 0: + raise TypeError( + "MapOverAsync mapping transformer must accept extra arguments plus the item." + ) + if len(params) == 1: + annotation = params[0].annotation + origin = get_origin(annotation) + args = get_args(annotation) + if origin is not tuple: + raise TypeError( + "MapOverAsync mapping transformer must accept a tuple of (extra, item). " + "Use MapAsync for single-argument mapping." + ) + if len(args) == 1 and args[0] is not ...: + raise TypeError( + "MapOverAsync mapping transformer tuple must include the extra argument " + "and the item." + ) + async def transform_async(self, *data: Unpack[Args]) -> list[_U]: lopping_result = [] for item in self.iterable: diff --git a/gloe/functional.py b/gloe/functional.py index 4b8090e3..aea03132 100644 --- a/gloe/functional.py +++ b/gloe/functional.py @@ -168,7 +168,7 @@ def transformer( @overload -def transformer(func: Callable[[], S]) -> Transformer[None, S]: +def transformer(func: Callable[[], S]) -> Transformer[S]: pass @@ -225,7 +225,7 @@ def async_transformer( @overload -def async_transformer(func: Callable[[], Awaitable[S]]) -> AsyncTransformer[None, S]: +def async_transformer(func: Callable[[], Awaitable[S]]) -> AsyncTransformer[S]: pass diff --git a/gloe/transformers.py b/gloe/transformers.py index 1c120309..bed1111a 100644 --- a/gloe/transformers.py +++ b/gloe/transformers.py @@ -7,7 +7,8 @@ from gloe.async_transformer import AsyncTransformer from gloe._transformer_utils import catch_transformer_exception -from gloe.base_transformer import BaseTransformer, Flow +from gloe.base_transformer import BaseTransformer, Flow, _NO_ARGS +from gloe.exceptions import TransformerRequiresMultiArgs from gloe._generic_types import ( AsyncNext2, @@ -97,6 +98,13 @@ def _safe_transform(self, data: object) -> _O: transformed: Optional[_O] = None try: args = cast(tuple[Unpack[Args]], self._unpack_call_args(data)) + except TransformerRequiresMultiArgs as exception: + raise TypeError(self._param_mismatch_message(data)) from exception + except TypeError as exception: + if data is _NO_ARGS: + raise TypeError(self._param_mismatch_message(data)) from exception + raise + try: transformed = self.transform(*args) except Exception as exception: transform_exception = catch_transformer_exception(exception, self) @@ -110,7 +118,7 @@ def _safe_transform(self, data: object) -> _O: raise NotImplementedError() # pragma: no cover @overload - def __call__(self: "Transformer[None, _O]") -> _O: + def __call__(self: "Transformer[_O]") -> _O: pass @overload diff --git a/tests/collection/test_transformer_collection.py b/tests/collection/test_transformer_collection.py index 3084a792..83712917 100644 --- a/tests/collection/test_transformer_collection.py +++ b/tests/collection/test_transformer_collection.py @@ -17,6 +17,13 @@ def many_args(arg1: str, arg2: int) -> str: self.assertListEqual(result, ["hello1", "world2"]) + def test_transformer_map_tuple_items(self): + mapping = Map(sum_tuple2) + + result = list(mapping([(1.0, 2.0), (3.0, 4.0)])) + + self.assertListEqual(result, [3.0, 7.0]) + def test_transformer_map(self): """ Test the map transformer @@ -79,3 +86,20 @@ def format_user(user: str, role: str) -> str: "User Alice has the role manager_role.", ], ) + + def test_transformer_map_over_multiargs(self): + data = [1.0, 2.0, 3.0] + + @transformer + def scale_add(scale: float, offset: float, item: float) -> float: + return scale * item + offset + + mapping = MapOver(data, scale_add) + + self.assertListEqual(mapping(2.0, 1.0), [3.0, 5.0, 7.0]) + + def test_transformer_map_over_item_only_raises(self): + with self.assertRaises(TypeError) as ctx: + MapOver([1.0, 2.0], square) # type: ignore[arg-type] + + self.assertIn("MapOver", str(ctx.exception)) diff --git a/tests/collection/test_transformer_collection_async.py b/tests/collection/test_transformer_collection_async.py index 80496ee0..b9206642 100644 --- a/tests/collection/test_transformer_collection_async.py +++ b/tests/collection/test_transformer_collection_async.py @@ -56,3 +56,22 @@ async def test_transformer_async_map_over(self): result = list(await mapping(-1.0)) self.assertListEqual(result, data) + + async def test_transformer_async_map_over_multiargs(self): + data = [1.0, 2.0, 3.0] + + @async_transformer + async def scale_add(scale: float, offset: float, item: float) -> float: + return scale * item + offset + + mapping = MapOverAsync(data, scale_add) + + result = await mapping(2.0, 1.0) + + self.assertListEqual(result, [3.0, 5.0, 7.0]) + + async def test_transformer_async_map_over_item_only_raises(self): + with self.assertRaises(TypeError) as ctx: + MapOverAsync([1.0, 2.0], async_plus1) # type: ignore[arg-type] + + self.assertIn("MapOverAsync", str(ctx.exception)) diff --git a/tests/collection/test_transformer_collection_types.py b/tests/collection/test_transformer_collection_types.py index 8fdc784d..1390f072 100644 --- a/tests/collection/test_transformer_collection_types.py +++ b/tests/collection/test_transformer_collection_types.py @@ -2,10 +2,10 @@ from typing_extensions import assert_type -from gloe import Transformer -from gloe.collection import Map, Filter +from gloe import Transformer, transformer, async_transformer +from gloe.collection import Map, Filter, MapOver, MapOverAsync from gloe.utils import forward -from tests.lib.transformers import format_currency, check_is_even +from tests.lib.transformers import format_currency, check_is_even, sum_tuple2 from tests.type_utils.mypy_test_suite import MypyTestSuite In = TypeVar("In") @@ -32,3 +32,35 @@ def test_transformer_filter(self): mapped_logarithm = forward[list[float]]() >> Filter(check_is_even) assert_type(mapped_logarithm, Transformer[list[float], list[float]]) + + def test_transformer_map_multiargs(self): + @transformer + def concat(arg1: str, arg2: int) -> str: + return arg1 + str(arg2) + + mapping = Map(concat) + + assert_type(mapping, Map[tuple[str, int], str]) + + def test_transformer_map_over_tuple_mapping(self): + mapping = MapOver([1.0, 2.0], sum_tuple2) + + assert_type(mapping, MapOver[float, float]) + + def test_transformer_map_over_multiargs(self): + @transformer + def scale_add(scale: float, offset: float, item: float) -> float: + return scale * item + offset + + mapping = MapOver([1.0, 2.0], scale_add) + + assert_type(mapping, MapOver[float, float, float]) + + def test_transformer_async_map_over_multiargs(self): + @async_transformer + async def scale_add(scale: float, offset: float, item: float) -> float: + return scale * item + offset + + mapping = MapOverAsync([1.0, 2.0], scale_add) + + assert_type(mapping, MapOverAsync[float, float, float]) diff --git a/tests/multiargs/test_multiargs_transformer_basic.py b/tests/multiargs/test_multiargs_transformer_basic.py index 7f6cbd5c..854cb2c1 100644 --- a/tests/multiargs/test_multiargs_transformer_basic.py +++ b/tests/multiargs/test_multiargs_transformer_basic.py @@ -59,6 +59,14 @@ def concat(arg1: str, arg2: str) -> str: with self.assertRaises(TransformerRequiresMultiArgs): concat("test") # type: ignore[call-arg] + def test_noargs_with_args_exception(self): + @transformer + def randint() -> int: + return 6 + + with self.assertRaises(TypeError): + randint(1) # type: ignore[call-overload] + def test_noargs_basic_call(self): @transformer def randint() -> int: @@ -74,3 +82,15 @@ def randfloat() -> float: pipeline = randfloat >> (square, square) >> sum_tuple2 self.assertEqual(pipeline(), 72.0) + + def test_composition_param_mismatch(self): + @transformer + def randint() -> int: + return 6 + + pipeline = randint >> sum_tuple2 # type: ignore[operator] + + with self.assertRaises(TypeError) as ctx: + pipeline() + + self.assertIn("parameter mismatch", str(ctx.exception)) diff --git a/tests/multiargs/test_multiargs_transformer_types.py b/tests/multiargs/test_multiargs_transformer_types.py index a1738960..41c09811 100644 --- a/tests/multiargs/test_multiargs_transformer_types.py +++ b/tests/multiargs/test_multiargs_transformer_types.py @@ -38,7 +38,7 @@ def test_noargs_transformer(self): def randint() -> int: return 6 - assert_type(randint, Transformer[None, int]) + assert_type(randint, Transformer[int]) def test_composition(self): @transformer diff --git a/tests/type_errors/test_type_errors.py b/tests/type_errors/test_type_errors.py new file mode 100644 index 00000000..e4c7b2d1 --- /dev/null +++ b/tests/type_errors/test_type_errors.py @@ -0,0 +1,62 @@ +import textwrap +import unittest +from mypy import api + + +class TestTypeErrors(unittest.TestCase): + def _run_mypy(self, code: str) -> str: + stdout, stderr, status = api.run(["-c", textwrap.dedent(code)]) + self.assertNotEqual(status, 0) + return stdout + stderr + + def test_noargs_to_tuple_composition_error(self): + output = self._run_mypy( + """ + from gloe import transformer + + @transformer + def randint() -> int: + return 6 + + @transformer + def sum_tuple2(num: tuple[float, float]) -> float: + return num[0] + num[1] + + randint >> sum_tuple2 + """ + ) + + self.assertIn("Unsupported operand types for >>", output) + + def test_mapover_item_only_error(self): + output = self._run_mypy( + """ + from gloe import transformer + from gloe.collection import MapOver + + @transformer + def square(num: float) -> float: + return num * num + + MapOver([1.0, 2.0], square) + """ + ) + + self.assertIn('Argument 2 to "MapOver" has incompatible type', output) + + def test_noargs_called_with_args_error(self): + output = self._run_mypy( + """ + from gloe import transformer + + @transformer + def randint() -> int: + return 6 + + randint(1) + """ + ) + + self.assertIn( + 'No overload variant of "__call__" of "Transformer"', output + ) From 6356bf1a11562673a66bad42c7d68efdc6f34899 Mon Sep 17 00:00:00 2001 From: Erick Lima Trentini Date: Sun, 25 Jan 2026 00:12:47 -0300 Subject: [PATCH 5/6] refactor: streamline argument handling in transformers and async transformers by removing overloads --- gloe/async_transformer.py | 12 +---- gloe/functional.py | 51 +++---------------- gloe/transformers.py | 12 +---- .../test_multiargs_transformer_basic.py | 2 +- tests/type_errors/test_type_errors.py | 2 +- 5 files changed, 11 insertions(+), 68 deletions(-) diff --git a/gloe/async_transformer.py b/gloe/async_transformer.py index 275ed1a8..b25cbd6d 100644 --- a/gloe/async_transformer.py +++ b/gloe/async_transformer.py @@ -117,17 +117,7 @@ def copy( ) -> Self: return self._copy(transform, regenerate_instance_id, "transform_async", force) - @overload - async def __call__(self: "AsyncTransformer[_Out]") -> _Out: - ... - - @overload - async def __call__( - self: "AsyncTransformer[Unpack[Args], _Out]", *data: Unpack[Args] - ) -> _Out: - ... - - async def __call__(self, *data): + async def __call__(self, *data: Unpack[Args]) -> _Out: packed = self._pack_call_args(data) return cast(_Out, await _execute_async_flow(self._flow, packed)) diff --git a/gloe/functional.py b/gloe/functional.py index aea03132..bd702527 100644 --- a/gloe/functional.py +++ b/gloe/functional.py @@ -17,9 +17,8 @@ A = TypeVar("A") S = TypeVar("S") -S2 = TypeVar("S2") P1 = ParamSpec("P1") -P2 = ParamSpec("P2") +Args = TypeVarTuple("Args") def partial_transformer( @@ -154,30 +153,9 @@ async def transform_async(self, data: A) -> S: return partial -Rest = TypeVarTuple("Rest") - - -B = TypeVar("B") - - -@overload def transformer( - func: Callable[[A, B, Unpack[Rest]], S], -) -> Transformer[A, B, Unpack[Rest], S]: - pass - - -@overload -def transformer(func: Callable[[], S]) -> Transformer[S]: - pass - - -@overload -def transformer(func: Callable[[A], S]) -> Transformer[A, S]: - pass - - -def transformer(func): + func: Callable[[Unpack[Args]], S], +) -> Transformer[Unpack[Args], S]: """ Convert a callable to an instance of the Transformer class. @@ -208,7 +186,7 @@ def signature(self) -> Signature: def transform(self, *data): if len(func_signature.parameters) == 0: - return func() + return cast(Callable[[], S], func)() return func(*data) lambda_transformer2 = LambdaTransformer() @@ -217,24 +195,9 @@ def transform(self, *data): return lambda_transformer2 -@overload def async_transformer( - func: Callable[[A, B, Unpack[Rest]], Awaitable[S]], -) -> AsyncTransformer[A, B, Unpack[Rest], S]: - pass - - -@overload -def async_transformer(func: Callable[[], Awaitable[S]]) -> AsyncTransformer[S]: - pass - - -@overload -def async_transformer(func: Callable[[A], Awaitable[S]]) -> AsyncTransformer[A, S]: - pass - - -def async_transformer(func): + func: Callable[[Unpack[Args]], Awaitable[S]], +) -> AsyncTransformer[Unpack[Args], S]: """ Convert a callable to an instance of the AsyncTransformer class. @@ -267,7 +230,7 @@ def signature(self) -> Signature: async def transform_async(self, *data): if len(func_signature.parameters) == 0: - return await func() + return await cast(Callable[[], Awaitable[S]], func)() return await func(*data) lambda_transformer = LambdaAsyncTransformer() diff --git a/gloe/transformers.py b/gloe/transformers.py index bed1111a..6a74f9cc 100644 --- a/gloe/transformers.py +++ b/gloe/transformers.py @@ -117,17 +117,7 @@ def _safe_transform(self, data: object) -> _O: raise NotImplementedError() # pragma: no cover - @overload - def __call__(self: "Transformer[_O]") -> _O: - pass - - @overload - def __call__( - self: "Transformer[Unpack[Args], _O]", *data: Unpack[Args] - ) -> _O: - pass - - def __call__(self, *data): + def __call__(self, *data: Unpack[Args]) -> _O: packed = self._pack_call_args(data) return cast(_O, _execute_flow(self._flow, packed)) diff --git a/tests/multiargs/test_multiargs_transformer_basic.py b/tests/multiargs/test_multiargs_transformer_basic.py index 854cb2c1..89295750 100644 --- a/tests/multiargs/test_multiargs_transformer_basic.py +++ b/tests/multiargs/test_multiargs_transformer_basic.py @@ -65,7 +65,7 @@ def randint() -> int: return 6 with self.assertRaises(TypeError): - randint(1) # type: ignore[call-overload] + randint(1) # type: ignore[call-arg] def test_noargs_basic_call(self): @transformer diff --git a/tests/type_errors/test_type_errors.py b/tests/type_errors/test_type_errors.py index e4c7b2d1..f78eb82b 100644 --- a/tests/type_errors/test_type_errors.py +++ b/tests/type_errors/test_type_errors.py @@ -58,5 +58,5 @@ def randint() -> int: ) self.assertIn( - 'No overload variant of "__call__" of "Transformer"', output + 'Too many arguments for "__call__" of "Transformer"', output ) From 19af8126c6014487b45f6b652484ee6847d30c19 Mon Sep 17 00:00:00 2001 From: Erick Lima Trentini Date: Sun, 25 Jan 2026 00:21:40 -0300 Subject: [PATCH 6/6] refactor: implement validate_mapover_signature utility and simplify transformer logic --- gloe/async_transformer.py | 15 +------- gloe/collection/_mapover.py | 31 ++------------- gloe/collection/_mapover_async.py | 31 ++------------- gloe/collection/_mapover_utils.py | 26 +++++++++++++ gloe/transformers.py | 63 +++++++++++++++---------------- 5 files changed, 66 insertions(+), 100 deletions(-) create mode 100644 gloe/collection/_mapover_utils.py diff --git a/gloe/async_transformer.py b/gloe/async_transformer.py index b25cbd6d..05ab19d0 100644 --- a/gloe/async_transformer.py +++ b/gloe/async_transformer.py @@ -85,9 +85,6 @@ def __repr__(self): ) async def _safe_transform(self, data: object) -> _Out: - transform_exception = None - - transformed: Optional[_Out] = None try: args = cast(tuple[Unpack[Args]], self._unpack_call_args(data)) except TransformerRequiresMultiArgs as exception: @@ -97,17 +94,9 @@ async def _safe_transform(self, data: object) -> _Out: raise TypeError(self._param_mismatch_message(data)) from exception raise try: - transformed = await self.transform_async(*args) + return await self.transform_async(*args) except Exception as exception: - transform_exception = catch_transformer_exception(exception, self) - - if transform_exception is not None: - raise transform_exception.internal_exception - - if type(transformed) is not None: - return cast(_Out, transformed) - - raise NotImplementedError # pragma: no cover + raise catch_transformer_exception(exception, self).internal_exception def copy( self, diff --git a/gloe/collection/_mapover.py b/gloe/collection/_mapover.py index cd8af791..d5afe4e5 100644 --- a/gloe/collection/_mapover.py +++ b/gloe/collection/_mapover.py @@ -4,6 +4,7 @@ from gloe.transformers import Transformer +from gloe.collection._mapover_utils import validate_mapover_signature Args = TypeVarTuple("Args") _Head = TypeVar("_Head") @@ -37,7 +38,9 @@ def __init__( | Transformer[_Head, Unpack[_Tail], _S, _U], ): super().__init__() - self._validate_mapping_transformer(mapping_transformer) + validate_mapover_signature( + mapping_transformer.signature(), "MapOver", "Map" + ) self.iterable = iterable self.mapping_transformer = cast( Transformer[tuple[object, ...], _U], mapping_transformer @@ -45,32 +48,6 @@ def __init__( self.plotting_settings.has_children = True self._children = [mapping_transformer] - def _validate_mapping_transformer( - self, - mapping_transformer: Transformer[tuple[_T, _S], _U] - | Transformer[_Head, Unpack[_Tail], _S, _U], - ) -> None: - mapping_signature = mapping_transformer.signature() - params = list(mapping_signature.parameters.values()) - if len(params) == 0: - raise TypeError( - "MapOver mapping transformer must accept extra arguments plus the item." - ) - if len(params) == 1: - annotation = params[0].annotation - origin = get_origin(annotation) - args = get_args(annotation) - if origin is not tuple: - raise TypeError( - "MapOver mapping transformer must accept a tuple of (extra, item). " - "Use Map for single-argument mapping." - ) - if len(args) == 1 and args[0] is not ...: - raise TypeError( - "MapOver mapping transformer tuple must include the extra argument " - "and the item." - ) - def signature(self) -> Signature: mapping_signature = self.mapping_transformer.signature() params = list(mapping_signature.parameters.values()) diff --git a/gloe/collection/_mapover_async.py b/gloe/collection/_mapover_async.py index 8afb2168..46ca3c0e 100644 --- a/gloe/collection/_mapover_async.py +++ b/gloe/collection/_mapover_async.py @@ -3,6 +3,7 @@ from typing_extensions import TypeVarTuple, Unpack from gloe import AsyncTransformer +from gloe.collection._mapover_utils import validate_mapover_signature Args = TypeVarTuple("Args") _Head = TypeVar("_Head") @@ -36,7 +37,9 @@ def __init__( | AsyncTransformer[_Head, Unpack[_Tail], _S, _U], ): super().__init__() - self._validate_mapping_transformer(mapping_transformer) + validate_mapover_signature( + mapping_transformer.signature(), "MapOverAsync", "MapAsync" + ) self.iterable = iterable self.mapping_transformer = cast( AsyncTransformer[tuple[object, ...], _U], mapping_transformer @@ -68,32 +71,6 @@ def signature(self) -> Signature: return Signature(parameters=params[:-1], return_annotation=return_annotation) - def _validate_mapping_transformer( - self, - mapping_transformer: AsyncTransformer[tuple[_T, _S], _U] - | AsyncTransformer[_Head, Unpack[_Tail], _S, _U], - ) -> None: - mapping_signature = mapping_transformer.signature() - params = list(mapping_signature.parameters.values()) - if len(params) == 0: - raise TypeError( - "MapOverAsync mapping transformer must accept extra arguments plus the item." - ) - if len(params) == 1: - annotation = params[0].annotation - origin = get_origin(annotation) - args = get_args(annotation) - if origin is not tuple: - raise TypeError( - "MapOverAsync mapping transformer must accept a tuple of (extra, item). " - "Use MapAsync for single-argument mapping." - ) - if len(args) == 1 and args[0] is not ...: - raise TypeError( - "MapOverAsync mapping transformer tuple must include the extra argument " - "and the item." - ) - async def transform_async(self, *data: Unpack[Args]) -> list[_U]: lopping_result = [] for item in self.iterable: diff --git a/gloe/collection/_mapover_utils.py b/gloe/collection/_mapover_utils.py new file mode 100644 index 00000000..9a62cf7b --- /dev/null +++ b/gloe/collection/_mapover_utils.py @@ -0,0 +1,26 @@ +from inspect import Signature +from typing import get_args, get_origin + + +def validate_mapover_signature( + signature: Signature, name: str, map_name: str +) -> None: + params = list(signature.parameters.values()) + if len(params) == 0: + raise TypeError( + f"{name} mapping transformer must accept extra arguments plus the item." + ) + if len(params) == 1: + annotation = params[0].annotation + origin = get_origin(annotation) + args = get_args(annotation) + if origin is not tuple: + raise TypeError( + f"{name} mapping transformer must accept a tuple of (extra, item). " + f"Use {map_name} for single-argument mapping." + ) + if len(args) == 1 and args[0] is not ...: + raise TypeError( + f"{name} mapping transformer tuple must include the extra argument " + "and the item." + ) diff --git a/gloe/transformers.py b/gloe/transformers.py index 6a74f9cc..ecde11df 100644 --- a/gloe/transformers.py +++ b/gloe/transformers.py @@ -1,9 +1,9 @@ from abc import ABC, abstractmethod from inspect import Signature -from typing import TypeVar, overload, cast, Optional, Iterable +from typing import TypeVar, overload, cast, Iterable -from typing_extensions import Generic, TypeAlias, Unpack, TypeVarTuple +from typing_extensions import Generic, Unpack, TypeVarTuple from gloe.async_transformer import AsyncTransformer from gloe._transformer_utils import catch_transformer_exception @@ -23,8 +23,6 @@ _O = TypeVar("_O", covariant=True) -Tr: TypeAlias = "Transformer" - Item = TypeVar("Item") O1 = TypeVar("O1") O2 = TypeVar("O2") @@ -93,9 +91,6 @@ def __repr__(self): ) def _safe_transform(self, data: object) -> _O: - transform_exception = None - - transformed: Optional[_O] = None try: args = cast(tuple[Unpack[Args]], self._unpack_call_args(data)) except TransformerRequiresMultiArgs as exception: @@ -107,15 +102,8 @@ def _safe_transform(self, data: object) -> _O: try: transformed = self.transform(*args) except Exception as exception: - transform_exception = catch_transformer_exception(exception, self) - - if transform_exception is not None: - raise transform_exception.internal_exception - - if type(transformed) is not None: - return cast(_O, transformed) - - raise NotImplementedError() # pragma: no cover + raise catch_transformer_exception(exception, self).internal_exception + return cast(_O, transformed) def __call__(self, *data: Unpack[Args]) -> _O: packed = self._pack_call_args(data) @@ -165,21 +153,26 @@ def __rshift__( @overload def __rshift__( self, - next_node: tuple["Tr[_O, O1]", "Tr[_O, O2]"], + next_node: tuple["Transformer[_O, O1]", "Transformer[_O, O2]"], ) -> "Transformer[Unpack[Args], tuple[O1, O2]]": pass @overload def __rshift__( self, - next_node: tuple["Tr[_O, O1]", "Tr[_O, O2]", "Tr[_O, O3]"], + next_node: tuple["Transformer[_O, O1]", "Transformer[_O, O2]", "Transformer[_O, O3]"], ) -> "Transformer[Unpack[Args], tuple[O1, O2, O3]]": pass @overload def __rshift__( self, - next_node: tuple["Tr[_O, O1]", "Tr[_O, O2]", "Tr[_O, O3]", "Tr[_O, O4]"], + next_node: tuple[ + "Transformer[_O, O1]", + "Transformer[_O, O2]", + "Transformer[_O, O3]", + "Transformer[_O, O4]", + ], ) -> "Transformer[Unpack[Args], tuple[O1, O2, O3, O4]]": pass @@ -187,7 +180,11 @@ def __rshift__( def __rshift__( self, next_node: tuple[ - "Tr[_O, O1]", "Tr[_O, O2]", "Tr[_O, O3]", "Tr[_O, O4]", "Tr[_O, O5]" + "Transformer[_O, O1]", + "Transformer[_O, O2]", + "Transformer[_O, O3]", + "Transformer[_O, O4]", + "Transformer[_O, O5]", ], ) -> "Transformer[Unpack[Args], tuple[O1, O2, O3, O4, O5]]": pass @@ -196,12 +193,12 @@ def __rshift__( def __rshift__( self, next_node: tuple[ - "Tr[_O, O1]", - "Tr[_O, O2]", - "Tr[_O, O3]", - "Tr[_O, O4]", - "Tr[_O, O5]", - "Tr[_O, O6]", + "Transformer[_O, O1]", + "Transformer[_O, O2]", + "Transformer[_O, O3]", + "Transformer[_O, O4]", + "Transformer[_O, O5]", + "Transformer[_O, O6]", ], ) -> "Transformer[Unpack[Args], tuple[O1, O2, O3, O4, O5, O6]]": pass @@ -210,13 +207,13 @@ def __rshift__( def __rshift__( self, next_node: tuple[ - "Tr[_O, O1]", - "Tr[_O, O2]", - "Tr[_O, O3]", - "Tr[_O, O4]", - "Tr[_O, O5]", - "Tr[_O, O6]", - "Tr[_O, O7]", + "Transformer[_O, O1]", + "Transformer[_O, O2]", + "Transformer[_O, O3]", + "Transformer[_O, O4]", + "Transformer[_O, O5]", + "Transformer[_O, O6]", + "Transformer[_O, O7]", ], ) -> "Transformer[Unpack[Args], tuple[O1, O2, O3, O4, O5, O6, O7]]": pass