diff --git a/.gitignore b/.gitignore index 09be0ef..7036d05 100644 --- a/.gitignore +++ b/.gitignore @@ -205,3 +205,9 @@ cython_debug/ marimo/_static/ marimo/_lsp/ __marimo__/ + +# Lcov +coverage.lcov +lcov.info +htmlcov/ +.coverage/ diff --git a/README.md b/README.md index 524adb8..65ac2c3 100644 --- a/README.md +++ b/README.md @@ -77,11 +77,7 @@ FloatTensor["features/2"] # Half the features dimension - `min(a,b)` Minimum of two expressions - `max(a,b)` Maximum of two expressions - -> [!WARNING] -> While nested function calls like `min(max(a,b),c)` are supported, -> combining function calls with other operators in the same expression -> (e.g., `min(1,batch)+max(2,channels)`) is not supported to simplify parsing. +- `isqrt(a)` Integer (floor) square root of a symbol or expression ### Symbolic Dimensions @@ -282,10 +278,10 @@ def free_function(tensor: FloatTensor["batch dim1"]) -> None: ## Limitations -- In the current implementation, _every_ call will be checked, which may or may not be slow depending on how big the context is (it shouldn't be that slow). +- In the current implementation, _every_ call will be checked, the performance overhead on most systems should be negligible (OTOO microseconds). - Pydantic default values are not checked. - Only symbolic, literal, and expressions are allowed for dimension specifiers, f-string syntax from `jaxtyping` is not supported. - Only torch tensors and numpy arrays are supported for now. -- Static checking is not supported, only runtime checks, though some errors will be caught statically by construction. +- Static shape checking is not supported, DLType only performs runtime checks, though some expression errors will be caught statically by construction if symbolic (i.e. non-string) shapes are used. - DLType does not support checkking inside unbounded container types (i.e. `list[TensorTypeBase]`) for performance reasons. - DLType does not support unions, but does support optionals. diff --git a/dltype/_lib/_core.py b/dltype/_lib/_core.py index d85f707..0e370e3 100644 --- a/dltype/_lib/_core.py +++ b/dltype/_lib/_core.py @@ -54,11 +54,13 @@ class DLTypeAnnotation(NamedTuple): def from_hint( cls, hint: type | None, + name: str, *, optional: bool = False, ) -> tuple[DLTypeAnnotation | None, ...]: """Create a new _DLTypeAnnotation from a type hint.""" if hint is None: + warnings.warn(f"[{name}] is missing a DLType hint", category=UserWarning, stacklevel=3) return (None,) _logger.debug("Creating DLType from hint %r", hint) @@ -77,11 +79,11 @@ def from_hint( raise TypeError(msg) # Recursively process the non-None type with optional=True - return cls.from_hint(non_none_types[0], optional=True) + return cls.from_hint(non_none_types[0], name, optional=True) # tuple handling special case if origin is tuple: - return tuple(itertools.chain(*[cls.from_hint(inner_hint) for inner_hint in args])) + return tuple(itertools.chain(*[cls.from_hint(inner_hint, name) for inner_hint in args])) # Only process Annotated types if origin is not Annotated: @@ -135,7 +137,7 @@ def _maybe_get_type_hints( return existing_hints try: return { - name: DLTypeAnnotation.from_hint(hint) + name: DLTypeAnnotation.from_hint(hint, name) for name, hint in get_type_hints(func, include_extras=True).items() } except NameError: @@ -209,7 +211,7 @@ def _inner_dltyped(func: Callable[P, R]) -> Callable[P, R]: # noqa: C901, PLR09 @wraps(func) @_dependency_utilities.torch_jit_unused # pyright: ignore[reportUnknownMemberType] - def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: # noqa: C901, PLR0912 + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: # noqa: C901 __tracebackhide__ = not _constants.DEBUG_MODE nonlocal signature nonlocal dltype_hints @@ -266,12 +268,6 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: # noqa: C901, PLR0912 _resolve_value(tensor, maybe_annotation), _resolve_types(maybe_annotation), ) - elif any(isinstance(actual_args[name], T) for T in _dtypes.SUPPORTED_TENSOR_TYPES): - warnings.warn( - f"[argument={name}] is missing a DLType hint", - UserWarning, - stacklevel=2, - ) else: _logger.debug("No DLType hint for %r", name) @@ -331,7 +327,7 @@ def _inner_dltyped_namedtuple(cls: type[NT]) -> type[NT]: for field_name in cls._fields: if field_name in field_hints: hint = field_hints[field_name] - dltype_fields[field_name] = DLTypeAnnotation.from_hint(hint) + dltype_fields[field_name] = DLTypeAnnotation.from_hint(hint, field_name) # If no fields need validation, return the original class if not dltype_fields: @@ -395,7 +391,7 @@ def _inner_dltyped_dataclass(cls: type[DataclassT]) -> type[DataclassT]: original_init = cls.__init__ # Get field annotations field_hints = get_type_hints(cls, include_extras=True) - dltype_hints = {name: DLTypeAnnotation.from_hint(hint) for name, hint in field_hints.items()} + dltype_hints = {name: DLTypeAnnotation.from_hint(hint, name) for name, hint in field_hints.items()} def new_init(self: DataclassT, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 """A new __init__ method that validates the fields after initialization.""" diff --git a/dltype/_lib/_parser.py b/dltype/_lib/_parser.py index 1149558..fb2e50b 100644 --- a/dltype/_lib/_parser.py +++ b/dltype/_lib/_parser.py @@ -3,29 +3,45 @@ from __future__ import annotations import enum -import itertools import logging import math import re -from typing import Final +import typing from typing_extensions import override -_logger: Final = logging.getLogger(__name__) +_logger: typing.Final = logging.getLogger(__name__) -class DLTypeSpecifier(enum.Enum): +class _DLTypeSpecifier(enum.Enum): """An enum representing a way to specify a name for a dimension expression or literal.""" EQUALS = "=" + def __repr__(self) -> str: + return self.value + + +class _DLTypeGroupToken(enum.Enum): + """An enum for grouping tokens.""" -class DLTypeModifier(enum.Enum): + LPAREN = "(" + RPAREN = ")" + COMMA = "," + + def __repr__(self) -> str: + return self.value + + +class _DLTypeModifier(enum.Enum): """An enum representing a modifier that can be applied to a dimension expression.""" ANONYMOUS_MULTIAXIS = "..." NAMED_MULTIAXIS = "*" + def __repr__(self) -> str: + return self.value + class _DLTypeOperator(enum.Enum): """An enum representing a mathematical operator for a dimension expression.""" @@ -39,6 +55,9 @@ class _DLTypeOperator(enum.Enum): MAX = "max" ISQRT = "isqrt" + def __repr__(self) -> str: + return self.value + def evaluate_unary(self, a: int) -> int: """Evaluate the unary operator.""" if self is _DLTypeOperator.ISQRT: @@ -64,32 +83,29 @@ def evaluate(self, a: int, b: int) -> int: # noqa: PLR0911 raise NotImplementedError(self) -_op_precedence: Final = { +_op_precedence: typing.Final = { _DLTypeOperator.ADD: 1, _DLTypeOperator.SUB: 1, _DLTypeOperator.MUL: 2, _DLTypeOperator.DIV: 2, _DLTypeOperator.EXP: 3, - _DLTypeOperator.MIN: 0, - _DLTypeOperator.MAX: 0, - _DLTypeOperator.ISQRT: 0, + _DLTypeOperator.MIN: 4, + _DLTypeOperator.MAX: 4, + _DLTypeOperator.ISQRT: 5, + _DLTypeGroupToken.LPAREN: 6, } -_unary_operators: Final = frozenset({_DLTypeOperator.ISQRT}) -_binary_operators: Final = frozenset({_DLTypeOperator.MIN, _DLTypeOperator.MAX}) -_functional_operators: Final = frozenset(_unary_operators.union(_binary_operators)) +_unary_functions: typing.Final = frozenset({_DLTypeOperator.ISQRT}) +_binary_functions: typing.Final = frozenset({_DLTypeOperator.MIN, _DLTypeOperator.MAX}) +_functional_operators: typing.Final = frozenset(_unary_functions.union(_binary_functions)) +_infix_operators: typing.Final = frozenset( + {_DLTypeOperator.ADD, _DLTypeOperator.SUB, _DLTypeOperator.MUL, _DLTypeOperator.DIV, _DLTypeOperator.EXP} +) _valid_operators: frozenset[str] = frozenset( {op.value for op in _DLTypeOperator if op not in _functional_operators}, ) -_valid_modifiers: frozenset[str] = frozenset({mod.value for mod in DLTypeModifier}) -INFIX_EXPRESSION_SPLIT_RX: Final = re.compile( - f"({'|'.join(map(re.escape, _valid_operators))})", -) -VALID_EXPRESSION_RX: Final = re.compile( - f"^[a-zA-Z0-9_{''.join(map(re.escape, _valid_operators.union(_valid_modifiers)))}]+$", -) -VALID_IDENTIFIER_RX: Final = re.compile(r"^[a-zA-Z][a-zA-Z0-9\_]*$") +_VALID_IDENTIFIER_RX: typing.Final = re.compile(r"^[a-zA-Z][a-zA-Z0-9\_]*$") class DLTypeDimensionExpression: @@ -102,6 +118,7 @@ def __init__( *, is_multiaxis_literal: bool = False, is_anonymous: bool = False, + is_named_multiaxis: bool = False, ) -> None: """Create a new dimension expression.""" self.identifier = identifier @@ -112,19 +129,20 @@ def __init__( self.is_literal = not is_multiaxis_literal and all( isinstance(token, int) for token in postfix_expression ) - self.is_identifier = is_multiaxis_literal or (postfix_expression == [identifier]) + self.is_identifier = ( + is_multiaxis_literal or is_named_multiaxis or (postfix_expression == [identifier]) + ) # this is an expression if it's not a literal value, if it's # an identifier that points to another dimension, or if it's an # identifier that doesn't just point to itself - self.is_expression = not self.is_literal and ( + self.is_expression = not (self.is_identifier and self.is_literal) and ( len(postfix_expression) > 1 or self.identifier not in postfix_expression ) self.is_multiaxis_literal = is_multiaxis_literal self.is_anonymous = is_anonymous + self.is_named_multiaxis = is_named_multiaxis _logger.debug( - "Created new %s dimension expression %r", - "multiaxis" if self.is_multiaxis_literal else "", - self, + "Created new %s dimension expression %r", "multiaxis" if self.is_multiaxis_literal else "", self ) # ensure we don't have any self-referential expressions @@ -186,7 +204,7 @@ def evaluate(self, scope: dict[str, int]) -> int: stack.append(scope[token]) elif isinstance(token, _DLTypeOperator): # pyright: ignore[reportUnnecessaryIsInstance] b = stack.pop() - if token in _unary_operators: + if token in _unary_functions: stack.append(token.evaluate_unary(b)) continue a = stack.pop() @@ -203,7 +221,38 @@ def evaluate(self, scope: dict[str, int]) -> int: return stack[0] -def _postfix_from_infix(identifier: str, expression: str) -> DLTypeDimensionExpression: +def _maybe_multiaxis( + identifier: str, + expression: list[TokenT | str | int], +) -> DLTypeDimensionExpression | None: + # this is a modified expression, so we need to handle it differently + if len(expression) == 1 and expression[0] == _DLTypeModifier.ANONYMOUS_MULTIAXIS.value: + return DLTypeDimensionExpression(identifier, [], is_anonymous=True) + + if len(expression) == 2 and expression[0] == _DLTypeOperator.MUL and isinstance(expression[1], str): # noqa: PLR2004 + if not _VALID_IDENTIFIER_RX.match(expression[1]): + msg = f"{expression[1]} is not a valid multiaxis identifier" + raise SyntaxError(msg) + return DLTypeDimensionExpression(expression[1], [expression[1]], is_named_multiaxis=True) + + return None + + +def _flush_op_by_precedence( + stack: list[str | _DLTypeOperator], + postfix: list[str | int | _DLTypeOperator], + current_op: _DLTypeOperator | _DLTypeGroupToken, +) -> None: + # Pop operators with higher or equal precedence + while ( + stack + and (isinstance(stack[-1], _DLTypeOperator | _DLTypeGroupToken)) + and _op_precedence.get(stack[-1], 0) >= _op_precedence.get(current_op, 0) + ): + postfix.append(stack.pop()) + + +def _postfix_from_infix(identifier: str, expression: list[TokenT | str | int]) -> DLTypeDimensionExpression: # noqa: C901, PLR0912, PLR0915 """ Extract a postfix expression from an infix expression. @@ -211,166 +260,192 @@ def _postfix_from_infix(identifier: str, expression: str) -> DLTypeDimensionExpr """ _logger.debug("Parsing infix expression %r", expression) - # this is a modified expression, so we need to handle it differently - if expression == DLTypeModifier.ANONYMOUS_MULTIAXIS.value: - return DLTypeDimensionExpression(expression, [], is_anonymous=True) - if expression.startswith(DLTypeModifier.NAMED_MULTIAXIS.value): - stripped_expression = expression[len(DLTypeModifier.NAMED_MULTIAXIS.value) :] - if not VALID_IDENTIFIER_RX.match(stripped_expression): - msg = f"Invalid identifier {stripped_expression=}" - raise SyntaxError(msg) - return DLTypeDimensionExpression(stripped_expression, [stripped_expression]) + if not expression: + msg = f"Argument list empty ({identifier})" + raise SyntaxError(msg) - split_expression = INFIX_EXPRESSION_SPLIT_RX.split(expression) + if maybe_multiaxis := _maybe_multiaxis(identifier, expression): + return maybe_multiaxis # Convert infix to postfix using shunting yard algorithm + scope_vars: set[str] = set() stack: list[str | _DLTypeOperator] = [] postfix: list[str | int | _DLTypeOperator] = [] - for token in split_expression: - if token.isdigit(): - postfix.append(int(token)) - elif token in _valid_operators: - current_op = _DLTypeOperator(token) - - # Pop operators with higher or equal precedence - while ( - stack - and isinstance(stack[-1], _DLTypeOperator) - and _op_precedence.get(stack[-1], 0) >= _op_precedence.get(current_op, 0) - ): - postfix.append(stack.pop()) + current_index = 0 + + while current_index < len(expression): + token = expression[current_index] + + if isinstance(token, int): + postfix.append(token) + current_index += 1 + elif token in _infix_operators: + current_op = token + _flush_op_by_precedence(stack, postfix, current_op) stack.append(current_op) - elif VALID_IDENTIFIER_RX.match(token): - # It's a variable name + current_index += 1 + elif token in _functional_operators or token == _DLTypeGroupToken.LPAREN: + current_op = token + assert isinstance(current_op, _DLTypeGroupToken | _DLTypeOperator) + _flush_op_by_precedence(stack, postfix, current_op) + + lparen, comma_indices, rparen = _get_group_indices(expression[current_index:], current_index) + if token in _binary_functions and len(comma_indices) != 1: + msg = f"{token.value} requires two arguments, received {len(comma_indices) + 1}" + raise SyntaxError(msg) + if token in _unary_functions and len(comma_indices) != 0: + msg = f"{token.value} requires one argument, received {len(comma_indices) + 1}" + raise SyntaxError(msg) + if token == _DLTypeGroupToken.LPAREN and len(comma_indices) != 0: + msg = "Group received invalid comma separator" + raise SyntaxError(msg) + + lhs = lparen + for arg_idx in [*comma_indices, rparen]: + inner_expr = _postfix_from_infix(f"{identifier}[{arg_idx}]", expression[lhs + 1 : arg_idx]) + postfix.extend(inner_expr.parsed_expression) + scope_vars.update(exp for exp in inner_expr.parsed_expression if isinstance(exp, str)) + lhs = arg_idx + + if current_op in _functional_operators: + stack.append(current_op) + current_index = rparen + 1 + elif isinstance(token, str) and _VALID_IDENTIFIER_RX.match(token): postfix.append(token) + scope_vars.add(token) + current_index += 1 else: - msg = f"Invalid expression {expression=}" + msg = f"Invalid expression={identifier} [{token=}] pos={current_index}/{len(expression)}" raise SyntaxError(msg) # Pop any remaining operators while stack: postfix.append(stack.pop()) - _logger.debug("Parsed infix expression %r to postfix %r", expression, postfix) + _logger.debug("Parsed infix expression %r to postfix %r", identifier, postfix) return DLTypeDimensionExpression(identifier, postfix) -def _maybe_parse_functional_expression( - identifier: str, - expression: str, - function: _DLTypeOperator, -) -> DLTypeDimensionExpression | None: - """ - Parse a function-like expression such as min(a,b) or max(x,y). - - Args: - identifier: The identifier for the expression (e.g. the name of the dimension) - expression: The expression to parse - function: The function operator (_DLTypeOperator.MIN or _DLTypeOperator.MAX) +TokenT: typing.TypeAlias = _DLTypeSpecifier | _DLTypeGroupToken | _DLTypeOperator - Returns: - A parsed dimension expression if the expression is a valid function call, None otherwise - """ - if not expression.startswith(f"{function.value}("): - return None +def _span_to_tok(character: str) -> TokenT | None: + maybe_operator = typing.cast("_DLTypeOperator | None", _DLTypeOperator._value2member_map_.get(character)) + maybe_specifier = typing.cast( + "_DLTypeSpecifier | None", _DLTypeSpecifier._value2member_map_.get(character) + ) + maybe_group = typing.cast("_DLTypeGroupToken | None", _DLTypeGroupToken._value2member_map_.get(character)) + return maybe_operator or maybe_specifier or maybe_group - # Find balanced closing parenthesis - # Strip function name and opening parenthesis - content = expression[len(function.value) + 1 :] - # Remove closing parenthesis - content = content[:-1] +def _span_to_str_or_int(span: str) -> str | int: + if span.isnumeric(): + return int(span) + return span - # Find the comma that separates arguments (accounting for nesting) - depth = 0 - balanced_content: list[str] = [] - current_span = "" - for char in content: - current_span += char - if char == "(": - depth += 1 - elif char == ")": - depth -= 1 - if depth < 0: - msg = f"Unbalanced parentheses in function expression: {expression}" - raise SyntaxError(msg) - elif char == "," and depth == 0: - balanced_content.append(current_span[:-1]) - current_span = "" - balanced_content.append(current_span) - - if function in _binary_operators and len(balanced_content) != 2: # noqa: PLR2004 - msg = f"Function {function.value} requires 2 arguments, got {len(balanced_content)} in {expression=}" +def _assert_token_list_valid(tokenized: list[str | int | TokenT]) -> None: + if len(tokenized) == 0: + msg = "Empty expression" raise SyntaxError(msg) - if function in _unary_operators and len(balanced_content) != 1: - msg = f"Function {function.value} requires 1 argument, got {len(balanced_content)} in {expression=}" - raise SyntaxError(msg) - - expressions = [expression_from_string(exp) for exp in balanced_content] - - # Build postfix expression: [arg1 tokens, arg2 tokens, function] - return DLTypeDimensionExpression( - identifier, - [*list(itertools.chain(*[exp.parsed_expression for exp in expressions])), function], - ) - - -def expression_from_string(expression: str) -> DLTypeDimensionExpression: - """ - Parse a dimension expression from a string and return a parsed expression. - Examples: - >>> expression_from_string("a+b") - Identifier + if len(tokenized) == 1 and isinstance(tokenized[0], str | int): + # special case where the only token is an identifier + return + + if len(tokenized) == 2 and tokenized[0] == _DLTypeOperator.MUL and isinstance(tokenized[1], str): # noqa: PLR2004 + # is an anonymous named axis + return + + # other than the special case above, fold the iterated list in to make sure the operators are balanced + n_expected_args = 1 # expected at least one expression + n_actual_args = 0 + + for tok in reversed(tokenized): + if tok in _unary_functions: + n_expected_args += 1 + n_actual_args += 1 + elif tok in _binary_functions | _infix_operators: + # all operators are binary + n_expected_args += 2 + n_actual_args += 1 + elif isinstance(tok, str | int): + n_actual_args += 1 + elif isinstance(tok, _DLTypeGroupToken): + continue + else: + raise SyntaxError(tok) - >>> expression_from_string("min(a,b)") - Identifier + if n_expected_args != n_actual_args: + raise SyntaxError("Invalid expression syntax: " + "".join(map(repr, tokenized))) - # literals - >>> expression_from_string("10") - Literal<10=[10]> - >>> expression_from_string("...") - Anonymous<...> +def _tokenize_string_expr( + expression: str, +) -> list[str | int | TokenT]: + return_list: list[str | int | TokenT] = [] + current_span = "" + for character in expression: + if character == " ": + msg = "Spaces not permitted in dimension expressions" + raise SyntaxError(msg) - >>> expression_from_string("a*10") - Identifier + if token := _span_to_tok(character): + if current_span: + return_list.append(_span_to_tok(current_span) or _span_to_str_or_int(current_span)) + current_span = "" + return_list.append(token) + else: + current_span += character + if current_span: + return_list.append(_span_to_tok(current_span) or _span_to_str_or_int(current_span)) + _assert_token_list_valid(return_list) + return return_list + + +def _get_group_indices(expr: list[TokenT | int | str], offset: int) -> tuple[int, list[int], int]: + lparen_idx: int | None = None + comma_idx: list[int] = [] + rparen_idx: int | None = None + nesting_depth = 0 + + for idx, tok in enumerate(expr): + if tok == _DLTypeGroupToken.LPAREN: + nesting_depth += 1 + lparen_idx = idx + offset if nesting_depth == 1 else lparen_idx + elif tok == _DLTypeGroupToken.COMMA and nesting_depth == 1: + comma_idx.append(idx + offset) + elif tok == _DLTypeGroupToken.RPAREN: + rparen_idx = idx + offset if nesting_depth == 1 else rparen_idx + nesting_depth -= 1 + + if rparen_idx: + break + + if lparen_idx is None: + msg = f"Invalid function syntax {expr=}, missing (" + raise SyntaxError(msg) + if rparen_idx is None: + msg = f"Invalid function syntax {expr=}, missing )" + raise SyntaxError(msg) - >>> expression_from_string("a*10+b") - Identifier + if lparen_idx > rparen_idx or any(c_idx < lparen_idx or c_idx > rparen_idx for c_idx in comma_idx): + msg = f"Unbalanced parenthesis in expression {''.join(map(repr, expr))}" + raise SyntaxError(msg) - Args: - identifier: The identifier for the expression (e.g. the name of the dimension) - expression: The expression to parse + return lparen_idx, comma_idx, rparen_idx - Returns: - A parsed dimension expression. - """ +def expression_from_string(expression: str) -> DLTypeDimensionExpression: + """Parse a dimension expression from a string and return a parsed expression.""" if not expression: msg = f"Empty expression {expression=}" raise SyntaxError(msg) # split the expression into the identifier and the expression if it has a specifier identifier = expression - if DLTypeSpecifier.EQUALS.value in expression: - identifier, expression = expression.split(DLTypeSpecifier.EQUALS.value) - - for function in _functional_operators: - if result := _maybe_parse_functional_expression( - identifier, - expression, - function, - ): - _logger.debug("Parsed function expression %r", result) - return result - - if not VALID_EXPRESSION_RX.match(expression): - msg = f"Invalid {expression=} {VALID_EXPRESSION_RX=}" - raise SyntaxError(msg) - - # split the expression into tokens using the operators from the enum as delimiters - return _postfix_from_infix(identifier, expression) + if _DLTypeSpecifier.EQUALS.value in expression: + identifier, expression = expression.split(_DLTypeSpecifier.EQUALS.value, maxsplit=1) + tokenized = _tokenize_string_expr(expression) + return _postfix_from_infix(identifier, tokenized) diff --git a/dltype/_lib/_symbolic_expressions.py b/dltype/_lib/_symbolic_expressions.py index 4e7d257..0660eb4 100644 --- a/dltype/_lib/_symbolic_expressions.py +++ b/dltype/_lib/_symbolic_expressions.py @@ -30,35 +30,35 @@ class Add(AxisOperationBase): def __str__(self) -> str: if isinstance(self._lhs, LiteralAxis) and isinstance(self._rhs, LiteralAxis): return f"{self._lhs.value + self._rhs.value}" - return f"{self._lhs}+{self._rhs}" + return f"({self._lhs}+{self._rhs})" class Subtract(AxisOperationBase): def __str__(self) -> str: if isinstance(self._lhs, LiteralAxis) and isinstance(self._rhs, LiteralAxis): return f"{self._lhs.value - self._rhs.value}" - return f"{self._lhs}-{self._rhs}" + return f"({self._lhs}-{self._rhs})" class Divide(AxisOperationBase): def __str__(self) -> str: if isinstance(self._lhs, LiteralAxis) and isinstance(self._rhs, LiteralAxis): return f"{self._lhs.value // self._rhs.value}" - return f"{self._lhs}/{self._rhs}" + return f"({self._lhs}/{self._rhs})" class Multiply(AxisOperationBase): def __str__(self) -> str: if isinstance(self._lhs, LiteralAxis) and isinstance(self._rhs, LiteralAxis): return f"{self._lhs.value * self._rhs.value}" - return f"{self._lhs}*{self._rhs}" + return f"({self._lhs}*{self._rhs})" class Exp(AxisOperationBase): def __str__(self) -> str: if isinstance(self._lhs, LiteralAxis) and isinstance(self._rhs, LiteralAxis): return f"{self._lhs.value**self._rhs.value}" - return f"{self._lhs}^{self._rhs}" + return f"({self._lhs}^{self._rhs})" class Max(AxisOperationBase): diff --git a/dltype/_lib/_tensor_type_base.py b/dltype/_lib/_tensor_type_base.py index c56ce06..039dc9b 100644 --- a/dltype/_lib/_tensor_type_base.py +++ b/dltype/_lib/_tensor_type_base.py @@ -88,31 +88,21 @@ def _parse_shape_string( # Process shape specification, looking for multiaxis modifiers processed_shapes: list[_parser.DLTypeDimensionExpression] = [] - modifiers: dict[int, _parser.DLTypeModifier | None] = {} + _multiaxis_parsed: set[int] = set() for i, dim_str in enumerate(split_shape): - modifiers[i] = None - for modifier in _parser.DLTypeModifier: - if dim_str.startswith(modifier.value): - modifiers[i] = modifier - break - - this_dimension_modifier = modifiers[i] - if this_dimension_modifier in { - _parser.DLTypeModifier.NAMED_MULTIAXIS, - _parser.DLTypeModifier.ANONYMOUS_MULTIAXIS, - }: - if self.multiaxis_index is not None: - msg = f"Multiple multiaxis modifiers not allowed in {shape_string=}" - raise SyntaxError(msg) - + expression = _parser.expression_from_string(dim_str) + if expression.is_named_multiaxis or expression.is_anonymous: + _multiaxis_parsed.add(i) + self.multiaxis_name = expression.identifier if expression.is_named_multiaxis else None self.multiaxis_index = i - self.multiaxis_name = dim_str[len(this_dimension_modifier.value) :] - self.anonymous_multiaxis = ( - this_dimension_modifier == _parser.DLTypeModifier.ANONYMOUS_MULTIAXIS - ) + self.anonymous_multiaxis |= expression.is_anonymous - processed_shapes.append(_parser.expression_from_string(dim_str)) + processed_shapes.append(expression) + + if len(_multiaxis_parsed) > 1: + msg = f"Multiple multiaxis modifiers not allowed in {shape_string=}" + raise SyntaxError(msg) return tuple(processed_shapes) diff --git a/dltype/tests/dltype_test.py b/dltype/tests/dltype_test.py index 65e4af3..e3d73fa 100644 --- a/dltype/tests/dltype_test.py +++ b/dltype/tests/dltype_test.py @@ -2,6 +2,7 @@ """Tests for common types used in deep learning.""" import re +import warnings from collections.abc import Callable from dataclasses import dataclass from pathlib import Path @@ -14,6 +15,7 @@ import pytest import torch from pydantic import BaseModel +from torch.jit import TracerWarning # pyright: ignore[reportPrivateImportUsage] import dltype @@ -28,6 +30,11 @@ class _RaisesInfo(NamedTuple): value: torch.Tensor | None = None +class _WarnsInfo(NamedTuple): + match_text: str + warning_type: type[Warning] = UserWarning + + @dltype.dltyped() def bad_function( tensor: Annotated[torch.Tensor, dltype.TensorTypeBase["b c h w"]], @@ -92,12 +99,13 @@ def bad_ndim_error(tensor_name: str, *, expected: int, actual: int) -> str: @pytest.mark.parametrize( - ("input_tensor", "func", "expected"), + ("input_tensor", "func", "expected", "maybe_warn"), [ pytest.param( torch.ones(1, 1, 1, 1), bad_function, _RaisesInfo(value=torch.ones(1, 1, 1, 1)), + None, id="bad_func trivial", ), pytest.param( @@ -107,6 +115,7 @@ def bad_ndim_error(tensor_name: str, *, expected: int, actual: int) -> str: exception_type=dltype.DLTypeShapeError, regex=bad_dimension_error("return", expected=3, idx=0, actual=1), ), + None, id="bad_func_4D", ), pytest.param( @@ -116,6 +125,7 @@ def bad_ndim_error(tensor_name: str, *, expected: int, actual: int) -> str: exception_type=dltype.DLTypeNDimsError, regex=bad_ndim_error("tensor", expected=4, actual=3), ), + None, id="bad_func_3D", ), pytest.param( @@ -125,12 +135,14 @@ def bad_ndim_error(tensor_name: str, *, expected: int, actual: int) -> str: exception_type=dltype.DLTypeNDimsError, regex=bad_ndim_error("tensor", expected=4, actual=5), ), + None, id="bad_func_5D", ), pytest.param( torch.ones(1, 2, 3, 4), good_function, _RaisesInfo(value=torch.ones(3, 4, 1, 2)), + None, id="good_func_4D", ), pytest.param( @@ -140,18 +152,21 @@ def bad_ndim_error(tensor_name: str, *, expected: int, actual: int) -> str: exception_type=dltype.DLTypeNDimsError, regex=bad_ndim_error("tensor", expected=4, actual=3), ), + None, id="good_func_3D", ), pytest.param( torch.ones(1, 2, 3, 4), incomplete_annotated_function, _RaisesInfo(value=torch.ones(1, 2, 3, 4)), + _WarnsInfo(match_text=re.escape("[return] is missing a DLType hint")), id="incomplete_annotated_4D", ), pytest.param( torch.ones(1, 2, 3, 4), incomplete_return_function, _RaisesInfo(value=torch.ones(3, 4, 1, 2)), + None, id="incomplete_return_4D", ), pytest.param( @@ -161,6 +176,7 @@ def bad_ndim_error(tensor_name: str, *, expected: int, actual: int) -> str: exception_type=RuntimeError, regex=r"number of dimensions in the tensor input does not match*", ), + None, id="invalid arg no type hint", ), ], @@ -169,6 +185,7 @@ def test_single_in_single_out( input_tensor: torch.Tensor, func: Callable[[torch.Tensor], torch.Tensor], expected: _RaisesInfo, + maybe_warn: _WarnsInfo | None, ) -> None: # test both positional and keyword arguments if expected.exception_type is not None: @@ -176,6 +193,11 @@ def test_single_in_single_out( func(input_tensor) with pytest.raises(expected.exception_type, match=expected.regex): func(tensor=input_tensor) # pyright: ignore[reportCallIssue] + elif maybe_warn is not None: + with pytest.warns(maybe_warn.warning_type, match=maybe_warn.match_text): + torch.testing.assert_close(func(input_tensor), expected.value) + with pytest.warns(maybe_warn.warning_type, match=maybe_warn.match_text): + torch.testing.assert_close(func(tensor=input_tensor), expected.value) # pyright: ignore[reportCallIssue] else: torch.testing.assert_close(func(input_tensor), expected.value) torch.testing.assert_close(func(tensor=input_tensor), expected.value) # pyright: ignore[reportCallIssue] @@ -506,7 +528,9 @@ def forward( ) -> Annotated[torch.Tensor, dltype.FloatTensor("b c h w")]: return torch.multiply(x, 2) - with NamedTemporaryFile() as f: + with NamedTemporaryFile() as f, warnings.catch_warnings(): + warnings.simplefilter(category=DeprecationWarning, action="ignore") + warnings.simplefilter(category=TracerWarning, action="ignore") torch.onnx.export( _DummyModule(), (torch.rand(1, 2, 3, 4),), @@ -518,7 +542,7 @@ def forward( assert Path(f.name).exists() assert Path(f.name).stat().st_size > 0 - with pytest.raises(TypeError): + with pytest.raises(dltype.DLTypeNDimsError): torch.onnx.export( _DummyModule(), (torch.rand(1, 2, 3),), @@ -539,23 +563,25 @@ def forward( _DummyModule().forward(torch.rand(1, 2, 3, 4)) - with pytest.raises(TypeError): + with pytest.raises(dltype.DLTypeNDimsError): _DummyModule().forward(torch.rand(1, 2, 3)) module = torch.compile(_DummyModule()) module(torch.rand(1, 2, 3, 4)) - with pytest.raises(TypeError): + with pytest.raises(dltype.DLTypeNDimsError): module(torch.rand(1, 2, 3)) - torch.jit.trace(_DummyModule(), torch.rand(1, 2, 3, 4)) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", TracerWarning) + torch.jit.trace(_DummyModule(), torch.rand(1, 2, 3, 4)) scripted_module = torch.jit.script(_DummyModule()) scripted_module(torch.rand(1, 2, 3, 4)) - with pytest.raises(TypeError): + with pytest.raises(dltype.DLTypeNDimsError): scripted_module(torch.rand(1, 2, 3)) @@ -583,7 +609,7 @@ def test_mixed_typing() -> None: torch.rand(2, 2, 2), ) - with pytest.raises(TypeError): + with pytest.raises(dltype.DLTypeShapeError): mixed_func( torch.rand(1, 2, 3, 4), np_rand(1, 2, 3, 4), @@ -622,7 +648,7 @@ def test_bad_dimension_name() -> None: def bad_function( # pyright: ignore[reportUnusedFunction] tensor: Annotated[torch.Tensor, dltype.TensorTypeBase["b?"]], ) -> None: - print(tensor) + pass @dltype.dltyped() @@ -719,17 +745,6 @@ def func_with_bad_expression( # pyright: ignore[reportUnusedFunction] ) -> None: return None - with pytest.raises(SyntaxError): - # don't allow multiple min/max calls - @dltype.dltyped() - def func_with_bad_expression( # pyright: ignore[reportUnusedFunction] - _: Annotated[ - torch.Tensor, - dltype.FloatTensor["batch channels min(1,channels-1)+max(channels,dim)"], - ], - ) -> None: - return None - with pytest.raises(SyntaxError): # don't allow multiple operators in a row @dltype.dltyped() @@ -924,20 +939,20 @@ def test_anonymous_wildcard_arg_and_return() -> None: # test that anonymous dimensions aren't matched assert result.shape[0] != input_shape[0] - with pytest.raises(TypeError): + with pytest.raises(dltype.DLTypeShapeError): bad_func_with_two_named_wildcards(torch.rand(1, 2, 3)) - with pytest.raises(TypeError): + with pytest.raises(dltype.DLTypeShapeError): bad_func_with_two_named_wildcards(torch.rand(1, 2, 3, 4, 5, 6)) func_with_named_wildcard_followed_by_literal(torch.rand(1, 1, 1, 3, 4)) func_with_named_wildcard_followed_by_literal(torch.rand(4, 3, 2, 1, 4, 5)) func_with_named_wildcard_followed_by_literal(torch.rand(1, 2, 3)) - with pytest.raises(TypeError): + with pytest.raises(dltype.DLTypeShapeError): func_with_named_wildcard_followed_by_literal(torch.rand(4, 3, 2, 2, 4, 5)) - with pytest.raises(TypeError): + with pytest.raises(dltype.DLTypeShapeError): func_with_named_wildcard_followed_by_literal(torch.rand(3, 2, 1)) @@ -1057,8 +1072,10 @@ def good_function( ) -> torch.Tensor: return tensor - good_function(torch.ones(1, 3, 4).int()) - good_function(torch.ones(4, 3, 4).int()) + with pytest.warns(UserWarning, match=re.escape("[return] is missing a DLType hint")): + good_function(torch.ones(1, 3, 4).int()) + with pytest.warns(UserWarning, match=re.escape("[return] is missing a DLType hint")): + good_function(torch.ones(4, 3, 4).int()) with pytest.raises(dltype.DLTypeShapeError): good_function(torch.ones(1, 3, 5).int()) @@ -1067,8 +1084,10 @@ def good_function( provider = Provider() - provider.forward(torch.ones(1, 3, 4)) - provider.forward(torch.ones(4, 3, 4)) + with pytest.warns(UserWarning, match=re.escape("[return] is missing a DLType hint")): + provider.forward(torch.ones(1, 3, 4)) + with pytest.warns(UserWarning, match=re.escape("[return] is missing a DLType hint")): + provider.forward(torch.ones(4, 3, 4)) with pytest.raises(dltype.DLTypeShapeError): provider.forward(torch.ones(1, 3, 5)) @@ -1089,12 +1108,14 @@ def optional_tensor_func( return tensor # Should work with None - result = optional_tensor_func(None) + with pytest.warns(UserWarning, match=re.escape("[return] is missing a DLType hint")): + result = optional_tensor_func(None) assert result.shape == (1, 3, 5, 5) # Should work with correct tensor input_tensor = torch.rand(2, 3, 4, 4) - torch.testing.assert_close(optional_tensor_func(input_tensor), input_tensor) + with pytest.warns(UserWarning, match=re.escape("[return] is missing a DLType hint")): + torch.testing.assert_close(optional_tensor_func(input_tensor), input_tensor) # Should fail with incorrect shape with pytest.raises(dltype.DLTypeNDimsError): @@ -1442,7 +1463,7 @@ def test_invalid_tensor_type_handling() -> None: def test_type_alias() -> None: @dltype.dltyped() def function(tensor: ShapedTensorT) -> None: - print(tensor) + assert tensor is not None function(torch.empty((1, 2, 3), dtype=torch.float16)) diff --git a/dltype/tests/parser_test.py b/dltype/tests/parser_test.py index 31d79e7..489329e 100644 --- a/dltype/tests/parser_test.py +++ b/dltype/tests/parser_test.py @@ -36,6 +36,18 @@ ("isqrt(x-y)", {"x": 20, "y": 5}, 3), ("min(isqrt(20),isqrt(16))", {}, 4), ("max(isqrt(20),isqrt(16))", {}, 4), + ("isqrt(min(3+2*3,10))", {}, 3), + ("min(2,3)+isqrt(16)", {}, 6), + ("min(1,isqrt(100))+isqrt(16)*isqrt(16)", {}, 17), + ("isqrt(16)*isqrt(16)+min(1,isqrt(100))", {}, 17), + ("isqrt(16)+isqrt(16)*min(1,isqrt(100))", {}, 8), + ("isqrt(16)/isqrt(z)", {"z": 16}, 1), + ("isqrt(16)/isqrt(z)", {"z": 4}, 2), + ("isqrt(min(1,100)+max(1,143))", {}, 12), + ("(a-b)/(b-c)", {"a": 10, "b": 2, "c": 0}, 4), + ("(a-1)*(b-1)+c", {"a": 10, "b": 2, "c": 10}, 19), + ("((a-1)*(b-1))+c", {"a": 10, "b": 2, "c": 10}, 19), + ("(a-1)*((b-1)+c)", {"a": 10, "b": 2, "c": 10}, 99), ], ) def test_parse_expression( @@ -57,10 +69,14 @@ def test_parse_expression( ("*batch", {}), ("3**2", {}), ("^", {}), - ("isqrt(4, 5)", {}), + ("isqrt(4,5)", {}), ("isqrt()", {}), ("max(1)", {}), ("min()", {}), + ("a=b=2", {}), + ("isqrt(*)", {}), + ("dim+", {}), + ("dim%", {}), ], ) def test_parse_invalid_expression(expression: str, scope: dict[str, int]) -> None: @@ -73,6 +89,7 @@ def test_parse_invalid_expression(expression: str, scope: dict[str, int]) -> Non [ (Shape[1 + 2], "3"), (Shape[Max(1, VariableAxis("imageh"))], "max(1,imageh)"), + (Shape[Max(1, 100)], "100"), (Shape[Min(1, 2)], "1"), (Shape[ConstantAxis("RGB", 4)], "RGB=4"), (Shape[..., VariableAxis("c"), VariableAxis("h"), VariableAxis("w")], "... c h w"), @@ -81,7 +98,21 @@ def test_parse_invalid_expression(expression: str, scope: dict[str, int]) -> Non "*batch c h w", ), (Shape[LiteralAxis(4), VariableAxis("r")], "4 r"), - (Shape[Min(4 + VariableAxis("image_w"), VariableAxis("imageh"))], "min(4+image_w,imageh)"), + (Shape[Min(4 + VariableAxis("image_w"), VariableAxis("imageh"))], "min((4+image_w),imageh)"), + (Shape[VariableAxis("a") - VariableAxis("b")], "(a-b)"), + (Shape[VariableAxis("a") + VariableAxis("b")], "(a+b)"), + (Shape[VariableAxis("a") * VariableAxis("b")], "(a*b)"), + (Shape[VariableAxis("a") ** VariableAxis("b")], "(a^b)"), + (Shape[VariableAxis("a") // VariableAxis("b")], "(a/b)"), + (Shape[LiteralAxis(99) - LiteralAxis(97)], "2"), + (Shape[LiteralAxis(12) + LiteralAxis(1)], "13"), + (Shape[LiteralAxis(10) * LiteralAxis(2)], "20"), + (Shape[LiteralAxis(3) ** LiteralAxis(3)], "27"), + (Shape[LiteralAxis(10) // LiteralAxis(2)], "5"), + (Shape[10 - VariableAxis("b")], "(10-b)"), + (Shape[10 // VariableAxis("b")], "(10/b)"), + (Shape[10 * VariableAxis("b")], "(10*b)"), + (Shape[10 ** VariableAxis("b")], "(10^b)"), ], ) def test_parse_symbolic(expression: Shape, expected: str) -> None: diff --git a/pyproject.toml b/pyproject.toml index 868381e..5fe8788 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,8 @@ dev = [ "ruff>=0.12.0", "torch>=1.4.0", "setuptools>=60.0.0", - "pyright>=1.1.407" + "pyright>=1.1.407", + "pytest-cov>=7.0.0" ] [project] @@ -18,7 +19,7 @@ license-files = ["LICENSE"] name = "dltype" readme = "README.md" requires-python = ">=3.10" -version = "0.7.0" +version = "0.8.0" [project.optional-dependencies] numpy = ["numpy"] @@ -35,6 +36,9 @@ include = ["dltype"] reportUnnecessaryTypeIgnoreComment = "error" typeCheckingMode = "strict" +[tool.pytest.ini_options] +addopts = "--cov=dltype --cov-report lcov:lcov.info --cov-report html" + [tool.ruff] indent-width = 4 line-length = 110 @@ -88,7 +92,7 @@ extend-select = [ "W" ] fixable = ["ALL"] -ignore = ["D203", "D212", "D401", "E501", "PLR6301", "PLR0917"] +ignore = ["COM812", "D203", "D212", "D401", "E501", "PLR6301", "PLR0917"] select = ["E4", "E7", "E9", "F"] [tool.ruff.lint.per-file-ignores] diff --git a/setup.sh b/setup.sh index 36275ab..f530a7f 100755 --- a/setup.sh +++ b/setup.sh @@ -10,7 +10,7 @@ uv sync if ! command -v pre-commit >/dev/null 2>&1 then echo "WARNING: pre-commit not found, please install it for a better dev experience" - echo "pip install pre-commit" + echo "pip install pre-commit --break-system-packages" echo "pre-commit install --install-hooks" else pre-commit install --install-hooks diff --git a/uv.lock b/uv.lock index f2866dd..9954511 100644 --- a/uv.lock +++ b/uv.lock @@ -25,9 +25,113 @@ 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 = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "dltype" -version = "0.7.0" +version = "0.8.0" source = { virtual = "." } dependencies = [ { name = "pydantic" }, @@ -49,6 +153,7 @@ dev = [ { name = "onnx" }, { name = "pyright" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "ruff" }, { name = "setuptools" }, { name = "torch" }, @@ -68,6 +173,7 @@ dev = [ { name = "onnx", specifier = ">=1.18.0" }, { name = "pyright", specifier = ">=1.1.407" }, { name = "pytest", specifier = ">=8.4.1" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "ruff", specifier = ">=0.12.0" }, { name = "setuptools", specifier = ">=60.0.0" }, { name = "torch", specifier = ">=1.4.0" }, @@ -698,6 +804,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + [[package]] name = "ruff" version = "0.12.0"