diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f7a9564c..686babd0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,8 +27,11 @@ jobs: enable-cache: true - name: Install just uses: extractions/setup-just@v3 + - name: Install graphviz + run: | + sudo apt-get update + sudo apt-get install graphviz graphviz-dev - run: just typing - - run: just typing-nb run-tests: diff --git a/CHANGELOG.md b/CHANGELOG.md index 36613b99..10be33a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and default pickle protocol. - {pull}`???` adapts the interactive debugger integration to Python 3.14's updated `pdb` behaviour and keeps pytest-style capturing intact. +- {pull}`734` migrates from mypy to ty for type checking. ## 0.5.7 - 2025-11-22 diff --git a/justfile b/justfile index f8e27aca..22f71be1 100644 --- a/justfile +++ b/justfile @@ -16,11 +16,7 @@ test-nb: # Run type checking typing: - uv run --group typing --no-dev --isolated mypy - -# Run type checking on notebooks -typing-nb: - uv run --group typing --no-dev --isolated nbqa mypy --ignore-missing-imports . + uv run --group typing ty check # Run linting lint: diff --git a/pyproject.toml b/pyproject.toml index 7f19dd16..4236ea4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,10 +73,12 @@ test = [ "pytest-xdist>=3.6.1", "syrupy>=4.5.0", "aiohttp>=3.11.0", # For HTTPPath tests. +] +typing = [ + "ty>=0.0.5", "coiled>=1.42.0", "cloudpickle>=3.0.0", ] -typing = ["mypy>=1.11.0", "nbqa>=1.8.5"] [project.urls] Changelog = "https://pytask-dev.readthedocs.io/en/stable/changes.html" @@ -167,33 +169,19 @@ filterwarnings = [ "ignore:The --rsyncdir command line argument:DeprecationWarning", ] -[tool.mypy] -files = ["src", "tests"] -check_untyped_defs = true -disallow_any_generics = true -disallow_incomplete_defs = true -disallow_untyped_defs = true -no_implicit_optional = true -warn_redundant_casts = true -warn_unused_ignores = true -disable_error_code = ["import-untyped"] - -[[tool.mypy.overrides]] -module = "tests.*" -disallow_untyped_defs = false -ignore_errors = true - -[[tool.mypy.overrides]] -module = ["click_default_group", "networkx"] -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = ["_pytask.coiled_utils"] -disable_error_code = ["import-not-found"] - -[[tool.mypy.overrides]] -module = ["_pytask.hookspecs"] -disable_error_code = ["empty-body"] +[tool.ty.src] +include = [ + "tests/test_build.py", + "tests/test_cache.py", + "tests/test_capture.py", + "tests/test_clean.py", + "tests/test_cli.py", + "tests/test_collect.py", +] +exclude = ["src/_pytask/_hashlib.py"] + +[tool.ty.terminal] +error-on-warning = true [tool.coverage.report] exclude_also = [ diff --git a/src/_pytask/build.py b/src/_pytask/build.py index ea242b59..83b4c3bd 100644 --- a/src/_pytask/build.py +++ b/src/_pytask/build.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING from typing import Any from typing import Literal +from typing import cast import click @@ -65,7 +66,7 @@ def pytask_unconfigure(session: Session) -> None: path.write_text(json.dumps(HashPathCache._cache)) -def build( # noqa: C901, PLR0912, PLR0913 +def build( # noqa: C901, PLR0912, PLR0913, PLR0915 *, capture: Literal["fd", "no", "sys", "tee-sys"] | CaptureMethod = CaptureMethod.FD, check_casing_of_paths: bool = True, @@ -230,10 +231,22 @@ def build( # noqa: C901, PLR0912, PLR0913 raw_config = {**DEFAULTS_FROM_CLI, **raw_config} - raw_config["paths"] = parse_paths(raw_config["paths"]) + paths_value = raw_config["paths"] + # Convert tuple to list since parse_paths expects Path | list[Path] + if isinstance(paths_value, tuple): + paths_value = list(paths_value) + if not isinstance(paths_value, (Path, list)): + msg = f"paths must be Path or list, got {type(paths_value)}" + raise TypeError(msg) # noqa: TRY301 + # Cast is justified - we validated at runtime + raw_config["paths"] = parse_paths(cast("Path | list[Path]", paths_value)) if raw_config["config"] is not None: - raw_config["config"] = Path(raw_config["config"]).resolve() + config_value = raw_config["config"] + if not isinstance(config_value, (str, Path)): + msg = f"config must be str or Path, got {type(config_value)}" + raise TypeError(msg) # noqa: TRY301 + raw_config["config"] = Path(config_value).resolve() raw_config["root"] = raw_config["config"].parent else: ( diff --git a/src/_pytask/cache.py b/src/_pytask/cache.py index 5c122833..6eea72aa 100644 --- a/src/_pytask/cache.py +++ b/src/_pytask/cache.py @@ -8,6 +8,9 @@ from inspect import FullArgSpec from typing import TYPE_CHECKING from typing import Any +from typing import ParamSpec +from typing import Protocol +from typing import TypeVar from attrs import define from attrs import field @@ -17,6 +20,23 @@ if TYPE_CHECKING: from collections.abc import Callable +P = ParamSpec("P") +R = TypeVar("R") + + +class MemoizedCallable(Protocol[P, R]): + """A callable that has been memoized and has a cache attribute. + + Note: We intentionally don't include __name__ or __module__ in the protocol + because not all callables have these attributes (e.g., functools.partial). + """ + + cache: Cache + + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: + """Call the memoized function.""" + ... + @define class CacheInfo: @@ -30,12 +50,14 @@ class Cache: _sentinel: Any = field(factory=object) cache_info: CacheInfo = field(factory=CacheInfo) - def memoize(self, func: Callable[..., Any]) -> Callable[..., Any]: - prefix = f"{func.__module__}.{func.__name__}:" + def memoize(self, func: Callable[P, R]) -> MemoizedCallable[P, R]: + func_module = getattr(func, "__module__", "") + func_name = getattr(func, "__name__", "") + prefix = f"{func_module}.{func_name}:" argspec = inspect.getfullargspec(func) @functools.wraps(func) - def wrapped(*args: Any, **kwargs: Any) -> Callable[..., Any]: + def wrapped(*args: P.args, **kwargs: P.kwargs) -> R: key = _make_memoize_key( args, kwargs, typed=False, argspec=argspec, prefix=prefix ) @@ -51,8 +73,7 @@ def wrapped(*args: Any, **kwargs: Any) -> Callable[..., Any]: return value wrapped.cache = self # type: ignore[attr-defined] - - return wrapped + return wrapped # type: ignore[return-value] def add(self, key: str, value: Any) -> None: self._cache[key] = value diff --git a/src/_pytask/capture.py b/src/_pytask/capture.py index 0e2a47fd..c4201b1f 100644 --- a/src/_pytask/capture.py +++ b/src/_pytask/capture.py @@ -129,7 +129,8 @@ def mode(self) -> str: # TextIOWrapper doesn't expose a mode, but at least some of our # tests check it. assert hasattr(self.buffer, "mode") - return cast("str", self.buffer.mode.replace("b", "")) + mode_value = cast("str", self.buffer.mode) + return mode_value.replace("b", "") class CaptureIO(io.TextIOWrapper): @@ -146,7 +147,7 @@ def __init__(self, other: TextIO) -> None: self._other = other super().__init__() - def write(self, s: str) -> int: + def write(self, s: str) -> int: # ty: ignore[invalid-method-override] super().write(s) return self._other.write(s) @@ -209,7 +210,7 @@ def truncate(self, size: int | None = None) -> int: # noqa: ARG002 msg = "Cannot truncate stdin." raise UnsupportedOperation(msg) - def write(self, data: str) -> int: # noqa: ARG002 + def write(self, data: str) -> int: # noqa: ARG002 # ty: ignore[invalid-method-override] msg = "Cannot write to stdin." raise UnsupportedOperation(msg) diff --git a/src/_pytask/click.py b/src/_pytask/click.py index 215a8c05..f5b643e1 100644 --- a/src/_pytask/click.py +++ b/src/_pytask/click.py @@ -37,8 +37,8 @@ if importlib.metadata.version("click") < "8.2": from click.parser import split_opt else: - from click.parser import ( # type: ignore[attr-defined, no-redef, unused-ignore] - _split_opt as split_opt, + from click.parser import ( # type: ignore[attr-defined, no-redef, unused-ignore, unresolved-import] + _split_opt as split_opt, # ty: ignore[unresolved-import] ) @@ -114,7 +114,7 @@ def format_help( else: formatted_name = Text(command_name, style="command") - commands_table.add_row(formatted_name, highlighter(command.help)) + commands_table.add_row(formatted_name, highlighter(command.help or "")) console.print( Panel( @@ -177,12 +177,13 @@ def parse_args(self, ctx: Context, args: list[str]) -> list[str]: _value, args = param.handle_parse_result(ctx, opts, args) if args and not ctx.allow_extra_args and not ctx.resilient_parsing: + args_list = list(args) if not isinstance(args, list) else args ctx.fail( ngettext( "Got unexpected extra argument ({args})", "Got unexpected extra arguments ({args})", len(args), - ).format(args=" ".join(map(str, args))) + ).format(args=" ".join(str(arg) for arg in args_list)) ) ctx.args = args diff --git a/src/_pytask/coiled_utils.py b/src/_pytask/coiled_utils.py index 7643933b..252caefa 100644 --- a/src/_pytask/coiled_utils.py +++ b/src/_pytask/coiled_utils.py @@ -26,9 +26,9 @@ class Function: # type: ignore[no-redef] def extract_coiled_function_kwargs(func: Function) -> dict[str, Any]: """Extract the kwargs for a coiled function.""" return { - "cluster_kwargs": func._cluster_kwargs, + "cluster_kwargs": func._cluster_kwargs, # ty: ignore[possibly-missing-attribute] "keepalive": func.keepalive, - "environ": func._environ, - "local": func._local, - "name": func._name, + "environ": func._environ, # ty: ignore[possibly-missing-attribute] + "local": func._local, # ty: ignore[possibly-missing-attribute] + "name": func._name, # ty: ignore[possibly-missing-attribute] } diff --git a/src/_pytask/collect.py b/src/_pytask/collect.py index d5db1403..764af8e7 100644 --- a/src/_pytask/collect.py +++ b/src/_pytask/collect.py @@ -52,6 +52,7 @@ from _pytask.task_utils import COLLECTED_TASKS from _pytask.task_utils import parse_collected_tasks_with_task_marker from _pytask.task_utils import task as task_decorator +from _pytask.typing import TaskFunction from _pytask.typing import is_task_function if TYPE_CHECKING: @@ -115,11 +116,11 @@ def _collect_from_tasks(session: Session) -> None: for raw_task in to_list(session.config.get("tasks", ())): if is_task_function(raw_task): - if not hasattr(raw_task, "pytask_meta"): + if not isinstance(raw_task, TaskFunction): raw_task = task_decorator()(raw_task) # noqa: PLW2901 path = get_file(raw_task) - name = raw_task.pytask_meta.name + name = raw_task.pytask_meta.name # ty: ignore[possibly-missing-attribute] if has_mark(raw_task, "task"): # When tasks with @task are passed to the programmatic interface @@ -339,7 +340,7 @@ def pytask_collect_task( markers = get_all_marks(obj) - if hasattr(obj, "pytask_meta"): + if isinstance(obj, TaskFunction): attributes = { **obj.pytask_meta.attributes, "collection_id": obj.pytask_meta._id, diff --git a/src/_pytask/collect_utils.py b/src/_pytask/collect_utils.py index 23451b2c..d9b7262c 100644 --- a/src/_pytask/collect_utils.py +++ b/src/_pytask/collect_utils.py @@ -21,6 +21,7 @@ from _pytask.tree_util import tree_leaves from _pytask.tree_util import tree_map_with_path from _pytask.typing import ProductType +from _pytask.typing import TaskFunction from _pytask.typing import no_default if TYPE_CHECKING: @@ -57,7 +58,7 @@ def parse_dependencies_from_task_function( """Parse dependencies from task function.""" dependencies = {} - task_kwargs = obj.pytask_meta.kwargs if hasattr(obj, "pytask_meta") else {} + task_kwargs = obj.pytask_meta.kwargs if isinstance(obj, TaskFunction) else {} signature_defaults = parse_keyword_arguments_from_signature_defaults(obj) kwargs = {**signature_defaults, **task_kwargs} kwargs.pop("produces", None) @@ -174,7 +175,7 @@ def parse_products_from_task_function( out: dict[str, Any] = {} - task_kwargs = obj.pytask_meta.kwargs if hasattr(obj, "pytask_meta") else {} + task_kwargs = obj.pytask_meta.kwargs if isinstance(obj, TaskFunction) else {} signature_defaults = parse_keyword_arguments_from_signature_defaults(obj) kwargs = {**signature_defaults, **task_kwargs} @@ -226,7 +227,7 @@ def parse_products_from_task_function( ) out[parameter_name] = collected_products - task_produces = obj.pytask_meta.produces if hasattr(obj, "pytask_meta") else None + task_produces = obj.pytask_meta.produces if isinstance(obj, TaskFunction) else None if task_produces: has_task_decorator = True collected_products = _collect_nodes_and_provisional_nodes( @@ -357,6 +358,6 @@ def create_name_of_python_node(node_info: NodeInfo) -> str: """Create name of PythonNode.""" node_name = node_info.task_name + "::" + node_info.arg_name if node_info.path: - suffix = "-".join(map(str, node_info.path)) + suffix = "-".join(str(p) for p in node_info.path) node_name += "::" + suffix return node_name diff --git a/src/_pytask/config_utils.py b/src/_pytask/config_utils.py index 64d4b4b0..7d1101c3 100644 --- a/src/_pytask/config_utils.py +++ b/src/_pytask/config_utils.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import TYPE_CHECKING from typing import Any +from typing import cast import click @@ -18,7 +19,7 @@ if sys.version_info >= (3, 11): # pragma: no cover import tomllib else: # pragma: no cover - import tomli as tomllib + import tomli as tomllib # ty: ignore[unresolved-import] __all__ = ["find_project_root_and_config", "read_config", "set_defaults_from_config"] @@ -51,7 +52,14 @@ def set_defaults_from_config( if not context.params["paths"]: context.params["paths"] = (Path.cwd(),) - context.params["paths"] = parse_paths(context.params["paths"]) + paths = context.params["paths"] + if isinstance(paths, tuple): + paths = list(paths) + if not isinstance(paths, (Path, list)): + msg = f"paths must be Path or list, got {type(paths)}" + raise TypeError(msg) + # Cast is justified - we validated at runtime + context.params["paths"] = parse_paths(cast("Path | list[Path]", paths)) ( context.params["root"], context.params["config"], diff --git a/src/_pytask/console.py b/src/_pytask/console.py index a0082f4b..8451ff66 100644 --- a/src/_pytask/console.py +++ b/src/_pytask/console.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import TYPE_CHECKING from typing import Any +from typing import cast from rich.console import Console from rich.console import RenderableType @@ -223,14 +224,18 @@ def get_file( # noqa: PLR0911 if hasattr(function, "__wrapped__"): source_file = inspect.getsourcefile(function) if source_file and Path(source_file) in skipped_paths: - return get_file(function.__wrapped__) + wrapped = cast("Callable[..., Any]", function.__wrapped__) + return get_file(wrapped) source_file = inspect.getsourcefile(function) if source_file: # pragma: no cover if "" in source_file or "ipykernel" in source_file: return None if "" in source_file: try: - return Path(function.__globals__["__file__"]).absolute().resolve() + globals_dict = cast( + "dict[str, Any]", getattr(function, "__globals__", {}) + ) + return Path(globals_dict["__file__"]).absolute().resolve() except KeyError: return None return Path(source_file).absolute().resolve() @@ -242,7 +247,8 @@ def _get_source_lines(function: Callable[..., Any]) -> int: if isinstance(function, functools.partial): return _get_source_lines(function.func) if hasattr(function, "__wrapped__"): - return _get_source_lines(function.__wrapped__) + wrapped = cast("Callable[..., Any]", function.__wrapped__) + return _get_source_lines(wrapped) return inspect.getsourcelines(function)[1] diff --git a/src/_pytask/dag_command.py b/src/_pytask/dag_command.py index b099c045..7e209622 100644 --- a/src/_pytask/dag_command.py +++ b/src/_pytask/dag_command.py @@ -6,6 +6,7 @@ import sys from pathlib import Path from typing import Any +from typing import cast import click import networkx as nx @@ -153,12 +154,24 @@ def build_dag(raw_config: dict[str, Any]) -> nx.DiGraph: # Add defaults from cli. from _pytask.cli import DEFAULTS_FROM_CLI # noqa: PLC0415 - raw_config = {**DEFAULTS_FROM_CLI, **raw_config} + raw_config = {**DEFAULTS_FROM_CLI, **raw_config} # ty: ignore[invalid-assignment] - raw_config["paths"] = parse_paths(raw_config["paths"]) + paths_value = raw_config["paths"] + # Convert tuple to list since parse_paths expects Path | list[Path] + if isinstance(paths_value, tuple): + paths_value = list(paths_value) + if not isinstance(paths_value, (Path, list)): + msg = f"paths must be Path or list, got {type(paths_value)}" + raise TypeError(msg) # noqa: TRY301 + # Cast is justified - we validated at runtime + raw_config["paths"] = parse_paths(cast("Path | list[Path]", paths_value)) if raw_config["config"] is not None: - raw_config["config"] = Path(raw_config["config"]).resolve() + config_value = raw_config["config"] + if not isinstance(config_value, (str, Path)): + msg = f"config must be str or Path, got {type(config_value)}" + raise TypeError(msg) # noqa: TRY301 + raw_config["config"] = Path(config_value).resolve() raw_config["root"] = raw_config["config"].parent else: ( @@ -183,9 +196,10 @@ def build_dag(raw_config: dict[str, Any]) -> nx.DiGraph: session = Session.from_config(config) - except (ConfigurationError, Exception): # noqa: BLE001 # pragma: no cover + except (ConfigurationError, Exception) as e: # pragma: no cover console.print_exception() - session = Session(exit_code=ExitCode.CONFIGURATION_FAILED) + msg = "Failed to configure session for dag." + raise ConfigurationError(msg) from e else: session.hook.pytask_log_session_header(session=session) diff --git a/src/_pytask/data_catalog.py b/src/_pytask/data_catalog.py index ed50a5b1..3f88a5f3 100644 --- a/src/_pytask/data_catalog.py +++ b/src/_pytask/data_catalog.py @@ -10,12 +10,11 @@ import inspect import pickle import re +from dataclasses import dataclass +from dataclasses import field from pathlib import Path from typing import Any -from attrs import define -from attrs import field - from _pytask.config_utils import find_project_root_and_config from _pytask.data_catalog_utils import DATA_CATALOG_NAME_FIELD from _pytask.exceptions import NodeNotCollectedError @@ -40,7 +39,7 @@ def _get_parent_path_of_data_catalog_module(stacklevel: int = 2) -> Path: return Path.cwd() -@define(kw_only=True) +@dataclass(kw_only=True) class DataCatalog: """A data catalog. @@ -61,28 +60,30 @@ class DataCatalog: """ default_node: type[PNode] = PickleNode - name: str = field(default="default") + name: str = "default" path: Path | None = None - _entries: dict[str, PNode | PProvisionalNode] = field(factory=dict) - _instance_path: Path = field(factory=_get_parent_path_of_data_catalog_module) + _entries: dict[str, PNode | PProvisionalNode] = field(default_factory=dict) + _instance_path: Path = field( + default_factory=_get_parent_path_of_data_catalog_module + ) _session_config: dict[str, Any] = field( - factory=lambda *x: {"check_casing_of_paths": True} # noqa: ARG005 + default_factory=lambda: {"check_casing_of_paths": True} ) - @name.validator - def _check(self, attribute: str, value: str) -> None: # noqa: ARG002 + def __post_init__(self) -> None: + # Validate name _rich_traceback_omit = True - if not isinstance(value, str): + if not isinstance(self.name, str): msg = "The name of a data catalog must be a string." raise TypeError(msg) - if not re.match(r"[a-zA-Z0-9-_]+", value): + if not re.match(r"[a-zA-Z0-9-_]+", self.name): msg = ( "The name of a data catalog must be a string containing only letters, " "numbers, hyphens, and underscores." ) raise ValueError(msg) - def __attrs_post_init__(self) -> None: + # Initialize paths and load persisted nodes root_path, _ = find_project_root_and_config((self._instance_path,)) self._session_config["paths"] = (root_path,) @@ -115,6 +116,7 @@ def add(self, name: str, node: PNode | PProvisionalNode | Any = None) -> None: if node is None: filename = hashlib.sha256(name.encode()).hexdigest() if isinstance(self.default_node, PPathNode): + assert self.path is not None self._entries[name] = self.default_node( name=name, path=self.path / f"{filename}.pkl" ) @@ -142,6 +144,6 @@ def add(self, name: str, node: PNode | PProvisionalNode | Any = None) -> None: node = self._entries[name] if hasattr(node, "attributes"): - node.attributes[DATA_CATALOG_NAME_FIELD] = self.name + node.attributes[DATA_CATALOG_NAME_FIELD] = self.name # ty: ignore[invalid-assignment] else: warn_about_upcoming_attributes_field_on_nodes() diff --git a/src/_pytask/debugging.py b/src/_pytask/debugging.py index 85e1dc8b..b986171f 100644 --- a/src/_pytask/debugging.py +++ b/src/_pytask/debugging.py @@ -114,7 +114,7 @@ def pytask_post_parse(config: dict[str, Any]) -> None: PytaskPDB._saved.append( (pdb.set_trace, PytaskPDB._pluginmanager, PytaskPDB._config) ) - pdb.set_trace = PytaskPDB.set_trace + pdb.set_trace = PytaskPDB.set_trace # ty: ignore[invalid-assignment] PytaskPDB._pluginmanager = config["pm"] PytaskPDB._config = config diff --git a/src/_pytask/hookspecs.py b/src/_pytask/hookspecs.py index 02ce0721..79804109 100644 --- a/src/_pytask/hookspecs.py +++ b/src/_pytask/hookspecs.py @@ -67,7 +67,7 @@ def pytask_extend_command_line_interface(cli: click.Group) -> None: @hookspec(firstresult=True) -def pytask_configure(pm: PluginManager, raw_config: dict[str, Any]) -> dict[str, Any]: +def pytask_configure(pm: PluginManager, raw_config: dict[str, Any]) -> dict[str, Any]: # type: ignore[invalid-return-type] """Configure pytask. The main hook implementation which controls the configuration and calls subordinated @@ -117,7 +117,7 @@ def pytask_collect(session: Session) -> Any: @hookspec(firstresult=True) -def pytask_ignore_collect(path: Path, config: dict[str, Any]) -> bool: +def pytask_ignore_collect(path: Path, config: dict[str, Any]) -> bool: # type: ignore[invalid-return-type] """Ignore collected path. This hook is indicates for each directory and file whether it should be ignored. @@ -138,7 +138,7 @@ def pytask_collect_modify_tasks(session: Session, tasks: list[PTask]) -> None: @hookspec(firstresult=True) def pytask_collect_file_protocol( session: Session, path: Path, reports: list[CollectionReport] -) -> list[CollectionReport]: +) -> list[CollectionReport]: # type: ignore[invalid-return-type] """Start protocol to collect files. The protocol calls the subordinate hook :func:`pytask_collect_file` which might @@ -166,7 +166,7 @@ def pytask_collect_file_log(session: Session, reports: list[CollectionReport]) - @hookspec(firstresult=True) def pytask_collect_task_protocol( session: Session, path: Path | None, name: str, obj: Any -) -> CollectionReport | None: +) -> CollectionReport | None: # type: ignore[invalid-return-type] """Start protocol to collect tasks.""" @@ -180,7 +180,7 @@ def pytask_collect_task_setup( @hookspec(firstresult=True) def pytask_collect_task( session: Session, path: Path | None, name: str, obj: Any -) -> PTask: +) -> PTask: # type: ignore[invalid-return-type] """Collect a single task.""" @@ -196,14 +196,14 @@ def pytask_collect_task_teardown(session: Session, task: PTask) -> None: @hookspec(firstresult=True) def pytask_collect_node( session: Session, path: Path, node_info: NodeInfo -) -> PNode | PProvisionalNode | None: +) -> PNode | PProvisionalNode | None: # type: ignore[invalid-return-type] """Collect a node which is a dependency or a product of a task.""" @hookspec(firstresult=True) def pytask_collect_log( session: Session, reports: list[CollectionReport], tasks: list[PTask] -) -> None: +) -> None: # type: ignore[invalid-return-type] """Log errors occurring during the collection. This hook reports errors during the collection. @@ -243,7 +243,7 @@ def pytask_execute_build(session: Session) -> Any: @hookspec(firstresult=True) -def pytask_execute_task_protocol(session: Session, task: PTask) -> ExecutionReport: +def pytask_execute_task_protocol(session: Session, task: PTask) -> ExecutionReport: # type: ignore[invalid-return-type] """Run the protocol for executing a test. This hook runs all stages of the execution process, setup, execution, and teardown diff --git a/src/_pytask/logging.py b/src/_pytask/logging.py index ce4bd6cb..cb309b88 100644 --- a/src/_pytask/logging.py +++ b/src/_pytask/logging.py @@ -137,7 +137,7 @@ def _format_duration(duration: float) -> str: i for i in duration_tuples if i[1] not in ("second", "seconds") ] - return ", ".join([" ".join(map(str, i)) for i in duration_tuples]) + return ", ".join([" ".join(str(x) for x in i) for i in duration_tuples]) def _humanize_time( # noqa: C901, PLR0912 diff --git a/src/_pytask/mark/expression.py b/src/_pytask/mark/expression.py index 20d3f902..ec415b9a 100644 --- a/src/_pytask/mark/expression.py +++ b/src/_pytask/mark/expression.py @@ -180,7 +180,7 @@ def not_expr(s: Scanner) -> ast.expr: if ident: return ast.Name(IDENT_PREFIX + ident.value, ast.Load()) s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT)) - return None + return None # ty: ignore[invalid-return-type] # Unreachable: reject() raises class MatcherAdapter(Mapping[str, bool]): diff --git a/src/_pytask/mark/structures.py b/src/_pytask/mark/structures.py index 76b5e2a8..2dbb61b3 100644 --- a/src/_pytask/mark/structures.py +++ b/src/_pytask/mark/structures.py @@ -10,6 +10,7 @@ from _pytask.mark_utils import get_all_marks from _pytask.models import CollectionMetadata +from _pytask.typing import TaskFunction from _pytask.typing import is_task_function if TYPE_CHECKING: @@ -166,7 +167,7 @@ def store_mark(obj: Callable[..., Any], mark: Mark) -> None: """ assert isinstance(mark, Mark), mark - if hasattr(obj, "pytask_meta"): + if isinstance(obj, TaskFunction): obj.pytask_meta.markers = [*get_unpacked_marks(obj), mark] else: obj.pytask_meta = CollectionMetadata( # type: ignore[attr-defined] diff --git a/src/_pytask/mark_utils.py b/src/_pytask/mark_utils.py index 4ee0e50c..f4f8b188 100644 --- a/src/_pytask/mark_utils.py +++ b/src/_pytask/mark_utils.py @@ -11,6 +11,7 @@ from _pytask.models import CollectionMetadata from _pytask.node_protocols import PTask +from _pytask.typing import TaskFunction if TYPE_CHECKING: from _pytask.mark import Mark @@ -21,14 +22,14 @@ def get_all_marks(obj_or_task: Any | PTask) -> list[Mark]: if isinstance(obj_or_task, PTask): return obj_or_task.markers obj = obj_or_task - return obj.pytask_meta.markers if hasattr(obj, "pytask_meta") else [] + return obj.pytask_meta.markers if isinstance(obj, TaskFunction) else [] def set_marks(obj_or_task: Any | PTask, marks: list[Mark]) -> Any | PTask: """Set marks on a callable or task.""" if isinstance(obj_or_task, PTask): obj_or_task.markers = marks - elif hasattr(obj_or_task, "pytask_meta"): + elif isinstance(obj_or_task, TaskFunction): obj_or_task.pytask_meta.markers = marks else: obj_or_task.pytask_meta = CollectionMetadata(markers=marks) diff --git a/src/_pytask/provisional.py b/src/_pytask/provisional.py index 85c355c7..48e3b29b 100644 --- a/src/_pytask/provisional.py +++ b/src/_pytask/provisional.py @@ -103,11 +103,14 @@ def pytask_execute_task(session: Session, task: PTask) -> None: session.hook.pytask_collect_modify_tasks( session=session, tasks=session.tasks ) + # Append the last collection report after successful modification + if report: + session.collection_reports.append(report) except Exception: # noqa: BLE001 # pragma: no cover - report = ExecutionReport.from_task_and_exception( + exec_report = ExecutionReport.from_task_and_exception( task=task, exc_info=sys.exc_info() ) - session.collection_reports.append(report) + session.execution_reports.append(exec_report) recreate_dag(session, task) diff --git a/src/_pytask/task_utils.py b/src/_pytask/task_utils.py index 9dbfb049..b5265a23 100644 --- a/src/_pytask/task_utils.py +++ b/src/_pytask/task_utils.py @@ -18,6 +18,7 @@ from _pytask.models import CollectionMetadata from _pytask.shared import find_duplicates from _pytask.shared import unwrap_task_function +from _pytask.typing import TaskFunction from _pytask.typing import is_task_function if TYPE_CHECKING: @@ -143,7 +144,7 @@ def wrapper(func: Callable[..., Any]) -> Callable[..., Any]: parsed_name = _parse_name(unwrapped, name) parsed_after = _parse_after(after) - if hasattr(unwrapped, "pytask_meta"): + if isinstance(unwrapped, TaskFunction): unwrapped.pytask_meta.after = parsed_after unwrapped.pytask_meta.is_generator = is_generator unwrapped.pytask_meta.id_ = id @@ -163,7 +164,7 @@ def wrapper(func: Callable[..., Any]) -> Callable[..., Any]: produces=produces, ) - if coiled_kwargs and hasattr(unwrapped, "pytask_meta"): + if coiled_kwargs and isinstance(unwrapped, TaskFunction): unwrapped.pytask_meta.attributes["coiled_kwargs"] = coiled_kwargs # Store it in the global variable ``COLLECTED_TASKS`` to avoid garbage @@ -188,7 +189,7 @@ def _parse_name(func: Callable[..., Any], name: str | None) -> str: func = func.func if hasattr(func, "__name__"): - return func.__name__ + return str(func.__name__) msg = "Cannot infer name for task function." raise NotImplementedError(msg) @@ -206,7 +207,7 @@ def _parse_after( if isinstance(after, list): new_after = [] for func in after: - if not hasattr(func, "pytask_meta"): + if not isinstance(func, TaskFunction): func = task()(func) # noqa: PLW2901 new_after.append(func.pytask_meta._id) # type: ignore[attr-defined] return new_after @@ -256,15 +257,16 @@ def _parse_tasks_with_preliminary_names( def _parse_task(task: Callable[..., Any]) -> tuple[str, Callable[..., Any]]: """Parse a single task.""" meta = task.pytask_meta # type: ignore[attr-defined] + task_name = getattr(task, "__name__", "_") - if meta.name is None and task.__name__ == "_": + if meta.name is None and task_name == "_": msg = ( "A task function either needs 'name' passed by the ``@task`` " "decorator or the function name of the task function must not be '_'." ) raise ValueError(msg) - parsed_name = task.__name__ if meta.name is None else meta.name + parsed_name = task_name if meta.name is None else meta.name parsed_kwargs = _parse_task_kwargs(meta.kwargs) signature_kwargs = parse_keyword_arguments_from_signature_defaults(task) diff --git a/src/_pytask/traceback.py b/src/_pytask/traceback.py index 9e1435af..dbe7174f 100644 --- a/src/_pytask/traceback.py +++ b/src/_pytask/traceback.py @@ -2,14 +2,14 @@ from __future__ import annotations +from dataclasses import dataclass +from dataclasses import field from pathlib import Path from types import TracebackType from typing import TYPE_CHECKING from typing import ClassVar import pluggy -from attrs import define -from attrs import field from rich.traceback import Traceback as RichTraceback import _pytask @@ -31,6 +31,8 @@ ] +assert pluggy.__file__ is not None +assert _pytask.__file__ is not None _PLUGGY_DIRECTORY = Path(pluggy.__file__).parent _PYTASK_DIRECTORY = Path(_pytask.__file__).parent @@ -41,10 +43,10 @@ OptionalExceptionInfo: TypeAlias = ExceptionInfo | tuple[None, None, None] -@define +@dataclass class Traceback: exc_info: OptionalExceptionInfo - show_locals: bool = field() + show_locals: bool = field(default_factory=lambda: Traceback._show_locals) _show_locals: ClassVar[bool] = False suppress: ClassVar[tuple[Path, ...]] = ( @@ -53,10 +55,6 @@ class Traceback: TREE_UTIL_LIB_DIRECTORY, ) - @show_locals.default - def _show_locals_default(self) -> bool: - return self._show_locals - def __rich_console__( self, console: Console, console_options: ConsoleOptions ) -> RenderResult: @@ -70,9 +68,12 @@ def __rich_console__( # The tracebacks returned by pytask-parallel are strings. if isinstance(filtered_exc_info[2], str): yield filtered_exc_info[2] - else: + elif filtered_exc_info[0] is not None and filtered_exc_info[1] is not None: yield RichTraceback.from_exception( - *filtered_exc_info, show_locals=self.show_locals + filtered_exc_info[0], + filtered_exc_info[1], + filtered_exc_info[2], + show_locals=self.show_locals, ) @@ -103,8 +104,14 @@ def _remove_internal_traceback_frames_from_exc_info( ) if isinstance(exc_info[2], TracebackType): - filtered_traceback = _filter_internal_traceback_frames(exc_info, suppress) - exc_info = (*exc_info[:2], filtered_traceback) + # If exc_info[2] is TracebackType, we know exc_info is ExceptionInfo, not + # (None, None, None) # noqa: ERA001 + assert exc_info[0] is not None + assert exc_info[1] is not None + # Create properly typed tuple for type checker + exception_info: ExceptionInfo = (exc_info[0], exc_info[1], exc_info[2]) + filtered_traceback = _filter_internal_traceback_frames(exception_info, suppress) + exc_info = (exc_info[0], exc_info[1], filtered_traceback) return exc_info diff --git a/src/_pytask/tree_util.py b/src/_pytask/tree_util.py index 834b0a05..ee8d137a 100644 --- a/src/_pytask/tree_util.py +++ b/src/_pytask/tree_util.py @@ -4,9 +4,11 @@ import functools from pathlib import Path +from typing import TYPE_CHECKING +from typing import Any +from typing import TypeVar import optree -from optree import PyTree from optree import tree_flatten_with_path as _optree_tree_flatten_with_path from optree import tree_leaves as _optree_tree_leaves from optree import tree_map as _optree_tree_map @@ -23,6 +25,20 @@ "tree_structure", ] +_T = TypeVar("_T") + +if TYPE_CHECKING: + # Use our own recursive type alias for static type checking. + # optree's PyTree uses __class_getitem__ to generate Union types at runtime, + # but type checkers like ty cannot evaluate these dynamic types properly. + # See: https://github.com/metaopt/optree/issues/251 + PyTree = ( + _T | tuple["PyTree[_T]", ...] | list["PyTree[_T]"] | dict[Any, "PyTree[_T]"] + ) +else: + from optree import PyTree + +assert optree.__file__ is not None TREE_UTIL_LIB_DIRECTORY = Path(optree.__file__).parent diff --git a/src/_pytask/typing.py b/src/_pytask/typing.py index d433ea06..097156ac 100644 --- a/src/_pytask/typing.py +++ b/src/_pytask/typing.py @@ -6,12 +6,15 @@ from typing import Any from typing import Final from typing import Literal +from typing import Protocol +from typing import runtime_checkable from attrs import define if TYPE_CHECKING: from typing import TypeAlias + from _pytask.models import CollectionMetadata from pytask import PTask @@ -19,11 +22,28 @@ "NoDefault", "Product", "ProductType", + "TaskFunction", "is_task_function", "no_default", ] +@runtime_checkable +class TaskFunction(Protocol): + """Protocol for callables decorated with @task that have pytask_meta attached. + + Note: This includes regular functions, functools.partial objects, and any other + callable that has been decorated with @task and has pytask_meta attached. + We don't require __name__ to support functools.partial. + """ + + pytask_meta: CollectionMetadata + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + """Call the task function.""" + ... + + @define(frozen=True) class ProductType: """A class to mark products.""" @@ -34,7 +54,7 @@ class ProductType: def is_task_function(obj: Any) -> bool: - """Check if an object is a task function.""" + """Check if an object could be decorated as a task function.""" return (callable(obj) and hasattr(obj, "__name__")) or ( isinstance(obj, functools.partial) and hasattr(obj.func, "__name__") ) diff --git a/src/pytask/__init__.py b/src/pytask/__init__.py index a71d6646..d994b403 100644 --- a/src/pytask/__init__.py +++ b/src/pytask/__init__.py @@ -74,6 +74,7 @@ from _pytask.task_utils import task from _pytask.traceback import Traceback from _pytask.typing import Product +from _pytask.typing import TaskFunction from _pytask.typing import is_task_function from _pytask.warnings_utils import WarningReport from _pytask.warnings_utils import parse_warning_filter @@ -131,6 +132,7 @@ "State", "Task", "TaskExecutionStatus", + "TaskFunction", "TaskOutcome", "TaskWithoutPath", "Traceback", diff --git a/tests/test_cache.py b/tests/test_cache.py index 218120f3..335623fc 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -36,7 +36,9 @@ def test_cache_add(): def func(a): return a - prefix = f"{func.__module__}.{func.__name__}:" + func_module = getattr(func, "__module__", "") + func_name = getattr(func, "__name__", "") + prefix = f"{func_module}.{func_name}:" argspec = inspect.getfullargspec(func) key = _make_memoize_key((1,), {}, typed=False, argspec=argspec, prefix=prefix) cache.add(key, 1) diff --git a/tests/test_capture.py b/tests/test_capture.py index a48c3ca3..afb25d60 100644 --- a/tests/test_capture.py +++ b/tests/test_capture.py @@ -556,7 +556,7 @@ def test_simple_resume_suspend(self): # Should not crash with missing "_old". assert repr(cap.syscapture) == ( " _state='done' tmpfile={!r}>".format( # noqa: UP032 - cap.syscapture.tmpfile + cap.syscapture.tmpfile # type: ignore[union-attr] ) ) diff --git a/tests/test_collect.py b/tests/test_collect.py index 0bb12ed9..36b27bc0 100644 --- a/tests/test_collect.py +++ b/tests/test_collect.py @@ -143,7 +143,7 @@ def test_error_with_invalid_file_name_pattern(runner, tmp_path): def test_error_with_invalid_file_name_pattern_(tmp_path): - session = build(paths=tmp_path, task_files=[1]) + session = build(paths=tmp_path, task_files=[1]) # type: ignore[arg-type] assert session.exit_code == ExitCode.CONFIGURATION_FAILED @@ -248,7 +248,9 @@ def test_find_shortest_uniquely_identifiable_names_for_tasks(tmp_path): for base_name in ("base_name_ident_0", "base_name_ident_1"): task = Task( - base_name=base_name, path=path_identifiable_by_base_name, function=None + base_name=base_name, + path=path_identifiable_by_base_name, + function=None, # type: ignore[arg-type] ) tasks.append(task) expected[task.name] = "t.py::" + base_name @@ -258,7 +260,7 @@ def test_find_shortest_uniquely_identifiable_names_for_tasks(tmp_path): for module in ("t.py", "m.py"): module_path = dir_identifiable_by_module_name / module - task = Task(base_name="task_a", path=module_path, function=None) + task = Task(base_name="task_a", path=module_path, function=None) # type: ignore[arg-type] tasks.append(task) expected[task.name] = module + "::task_a" @@ -270,7 +272,7 @@ def test_find_shortest_uniquely_identifiable_names_for_tasks(tmp_path): for base_path in (dir_identifiable_by_folder_a, dir_identifiable_by_folder_b): module_path = base_path / "t.py" - task = Task(base_name="task_t", path=module_path, function=None) + task = Task(base_name="task_t", path=module_path, function=None) # type: ignore[arg-type] tasks.append(task) expected[task.name] = base_path.name + "/t.py::task_t" @@ -308,7 +310,8 @@ def test_collect_tasks_from_modules_with_the_same_name(tmp_path): for report in session.collection_reports ) assert { - report.node.function.__module__ for report in session.collection_reports + report.node.function.__module__ # type: ignore[union-attr] + for report in session.collection_reports } == {"a.task_module", "b.task_module"} diff --git a/tests/test_collect_command.py b/tests/test_collect_command.py index a615648c..0a4c76b5 100644 --- a/tests/test_collect_command.py +++ b/tests/test_collect_command.py @@ -306,7 +306,7 @@ def task_example_2(path=Path("in_2.txt"), produces=Path("out_2.txt")): ... @define class Node: - path: Path + path: str def state(self): ... @@ -321,8 +321,8 @@ def test_print_collected_tasks_without_nodes(capsys): base_name="function", path=Path("task_path.py"), function=function, - depends_on={0: Node("in.txt")}, - produces={0: Node("out.txt")}, + depends_on={"depends_on": Node("in.txt")}, + produces={"produces": Node("out.txt")}, ) ] } @@ -344,7 +344,7 @@ def test_print_collected_tasks_with_nodes(capsys): path=Path("task_path.py"), function=function, depends_on={"depends_on": PathNode(name="in.txt", path=Path("in.txt"))}, - produces={0: PathNode(name="out.txt", path=Path("out.txt"))}, + produces={"produces": PathNode(name="out.txt", path=Path("out.txt"))}, ) ] } @@ -370,7 +370,7 @@ def test_find_common_ancestor_of_all_nodes(show_nodes, expected_add): "depends_on": PathNode.from_path(Path.cwd() / "src" / "in.txt") }, produces={ - 0: PathNode.from_path( + "produces": PathNode.from_path( Path.cwd().joinpath("..", "bld", "out.txt").resolve() ) }, diff --git a/uv.lock b/uv.lock index dfaaaac2..8430a6d6 100644 --- a/uv.lock +++ b/uv.lock @@ -215,19 +215,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/bc/3f66af9beb683728e06ca08797e4e9d3e44f432f339718cae3ba856a9cad/autodocsumm-0.2.14-py3-none-any.whl", hash = "sha256:3bad8717fc5190802c60392a7ab04b9f3c97aa9efa8b3780b3d81d615bfe5dc0", size = 14640, upload-time = "2024-10-23T18:51:45.115Z" }, ] -[[package]] -name = "autopep8" -version = "2.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycodestyle" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/d8/30873d2b7b57dee9263e53d142da044c4600a46f2d28374b3e38b023df16/autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758", size = 92210, upload-time = "2025-01-14T14:46:18.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128", size = 45807, upload-time = "2025-01-14T14:46:15.466Z" }, -] - [[package]] name = "babel" version = "2.17.0" @@ -2023,60 +2010,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/5d/e17845bb0fa76334477d5de38654d27946d5b5d3695443987a094a71b440/multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac", size = 10481, upload-time = "2025-05-19T14:16:36.024Z" }, ] -[[package]] -name = "mypy" -version = "1.18.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" }, - { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" }, - { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" }, - { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" }, - { url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" }, - { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, - { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, - { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, - { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, - { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, - { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, - { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, - { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, - { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, - { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, - { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, - { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, - { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, -] - -[[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 = "myst-nb" version = "1.2.0" @@ -2173,22 +2106,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl", hash = "sha256:c6fbe6e48b60cacac14af40b38bf338a3b88f47f085c54ac5b8639ff0babaf4b", size = 12818, upload-time = "2024-12-23T18:33:44.566Z" }, ] -[[package]] -name = "nbqa" -version = "1.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "autopep8" }, - { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "tokenize-rt" }, - { name = "tomli" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/76/62d2609924cf34445148cd6b5de694cf64c179cc416cac93182579620e57/nbqa-1.9.1.tar.gz", hash = "sha256:a1f4bcf587c597302fed295951001fc4e1be4ce0e77e1ab1b25ac2fbe3db0cdd", size = 38348, upload-time = "2024-11-10T12:21:58.333Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/88/4789719fbbe166d12d345b3ac66b96105f10001b16e00a9765ba29261a21/nbqa-1.9.1-py3-none-any.whl", hash = "sha256:95552d2f6c2c038136252a805aa78d85018aef922586270c3a074332737282e5", size = 35259, upload-time = "2024-11-10T12:21:56.731Z" }, -] - [[package]] name = "nest-asyncio" version = "1.6.0" @@ -2408,15 +2325,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, ] -[[package]] -name = "pathspec" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, -] - [[package]] name = "pexpect" version = "4.9.0" @@ -2689,15 +2597,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] -[[package]] -name = "pycodestyle" -version = "2.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/6e/1f4a62078e4d95d82367f24e685aef3a672abfd27d1a868068fed4ed2254/pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae", size = 39312, upload-time = "2025-03-29T17:33:30.669Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/be/b00116df1bfb3e0bb5b45e29d604799f7b91dd861637e4d448b4e09e6a3e/pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9", size = 31424, upload-time = "2025-03-29T17:33:29.405Z" }, -] - [[package]] name = "pycparser" version = "2.22" @@ -2798,8 +2697,6 @@ plugin-list = [ ] test = [ { name = "aiohttp" }, - { name = "cloudpickle" }, - { name = "coiled" }, { name = "deepdiff" }, { name = "nbmake" }, { name = "pexpect" }, @@ -2810,8 +2707,9 @@ test = [ { name = "syrupy" }, ] typing = [ - { name = "mypy" }, - { name = "nbqa" }, + { name = "cloudpickle" }, + { name = "coiled" }, + { name = "ty" }, ] [package.metadata] @@ -2854,8 +2752,6 @@ plugin-list = [ ] test = [ { name = "aiohttp", specifier = ">=3.11.0" }, - { name = "cloudpickle", specifier = ">=3.0.0" }, - { name = "coiled", specifier = ">=1.42.0" }, { name = "deepdiff", specifier = ">=7.0.0" }, { name = "nbmake", specifier = ">=1.5.5" }, { name = "pexpect", specifier = ">=4.9.0" }, @@ -2866,8 +2762,9 @@ test = [ { name = "syrupy", specifier = ">=4.5.0" }, ] typing = [ - { name = "mypy", specifier = ">=1.11.0" }, - { name = "nbqa", specifier = ">=1.8.5" }, + { name = "cloudpickle", specifier = ">=3.0.0" }, + { name = "coiled", specifier = ">=1.42.0" }, + { name = "ty", specifier = ">=0.0.5" }, ] [[package]] @@ -3760,15 +3657,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/44/aa5c8b10b2cce7a053018e0d132bd58e27527a0243c4985383d5b6fd93e9/tblib-3.1.0-py3-none-any.whl", hash = "sha256:670bb4582578134b3d81a84afa1b016128b429f3d48e6cbbaecc9d15675e984e", size = 12552, upload-time = "2025-03-31T12:58:26.142Z" }, ] -[[package]] -name = "tokenize-rt" -version = "6.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/ed/8f07e893132d5051d86a553e749d5c89b2a4776eb3a579b72ed61f8559ca/tokenize_rt-6.2.0.tar.gz", hash = "sha256:8439c042b330c553fdbe1758e4a05c0ed460dbbbb24a606f11f0dee75da4cad6", size = 5476, upload-time = "2025-05-23T23:48:00.035Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl", hash = "sha256:a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44", size = 6004, upload-time = "2025-05-23T23:47:58.812Z" }, -] - [[package]] name = "toml" version = "0.10.2" @@ -3866,6 +3754,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] +[[package]] +name = "ty" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/db/6299d478000f4f1c6f9bf2af749359381610ffc4cbe6713b66e436ecf6e7/ty-0.0.5.tar.gz", hash = "sha256:983da6330773ff71e2b249810a19c689f9a0372f6e21bbf7cde37839d05b4346", size = 4806218, upload-time = "2025-12-20T21:19:17.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/c1f61ba378b4191e641bb36c07b7fcc70ff844d61be7a4bf2fea7472b4a9/ty-0.0.5-py3-none-linux_armv6l.whl", hash = "sha256:1594cd9bb68015eb2f5a3c68a040860f3c9306dc6667d7a0e5f4df9967b460e2", size = 9785554, upload-time = "2025-12-20T21:19:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f9/b37b77c03396bd779c1397dae4279b7ad79315e005b3412feed8812a4256/ty-0.0.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7c0140ba980233d28699d9ddfe8f43d0b3535d6a3bbff9935df625a78332a3cf", size = 9603995, upload-time = "2025-12-20T21:19:15.256Z" }, + { url = "https://files.pythonhosted.org/packages/7d/70/4e75c11903b0e986c0203040472627cb61d6a709e1797fb08cdf9d565743/ty-0.0.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:15de414712cde92048ae4b1a77c4dc22920bd23653fe42acaf73028bad88f6b9", size = 9145815, upload-time = "2025-12-20T21:19:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/89/05/93983dfcf871a41dfe58e5511d28e6aa332a1f826cc67333f77ae41a2f8a/ty-0.0.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:438aa51ad6c5fae64191f8d58876266e26f9250cf09f6624b6af47a22fa88618", size = 9619849, upload-time = "2025-12-20T21:19:19.084Z" }, + { url = "https://files.pythonhosted.org/packages/82/b6/896ab3aad59f846823f202e94be6016fb3f72434d999d2ae9bd0f28b3af9/ty-0.0.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b3d373fd96af1564380caf153600481c676f5002ee76ba8a7c3508cdff82ee0", size = 9606611, upload-time = "2025-12-20T21:19:24.583Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ae/098e33fc92330285ed843e2750127e896140c4ebd2d73df7732ea496f588/ty-0.0.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8453692503212ad316cf8b99efbe85a91e5f63769c43be5345e435a1b16cba5a", size = 10029523, upload-time = "2025-12-20T21:19:07.055Z" }, + { url = "https://files.pythonhosted.org/packages/04/5a/f4b4c33758b9295e9aca0de9645deca0f4addd21d38847228723a6e780fc/ty-0.0.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2e4c454139473abbd529767b0df7a795ed828f780aef8d0d4b144558c0dc4446", size = 10870892, upload-time = "2025-12-20T21:19:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c5/4e3e7e88389365aa1e631c99378711cf0c9d35a67478cb4720584314cf44/ty-0.0.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:426d4f3b82475b1ec75f3cc9ee5a667c8a4ae8441a09fcd8e823a53b706d00c7", size = 10599291, upload-time = "2025-12-20T21:19:26.557Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5d/138f859ea87bd95e17b9818e386ae25a910e46521c41d516bf230ed83ffc/ty-0.0.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5710817b67c6b2e4c0224e4f319b7decdff550886e9020f6d46aa1ce8f89a609", size = 10413515, upload-time = "2025-12-20T21:19:11.094Z" }, + { url = "https://files.pythonhosted.org/packages/27/21/1cbcd0d3b1182172f099e88218137943e0970603492fb10c7c9342369d9a/ty-0.0.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23c55ef08882c7c5ced1ccb90b4eeefa97f690aea254f58ac0987896c590f76", size = 10144992, upload-time = "2025-12-20T21:19:13.225Z" }, + { url = "https://files.pythonhosted.org/packages/ad/30/fdac06a5470c09ad2659a0806497b71f338b395d59e92611f71b623d05a0/ty-0.0.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b9e4c1a28a23b14cf8f4f793f4da396939f16c30bfa7323477c8cc234e352ac4", size = 9606408, upload-time = "2025-12-20T21:19:09.212Z" }, + { url = "https://files.pythonhosted.org/packages/09/93/e99dcd7f53295192d03efd9cbcec089a916f49cad4935c0160ea9adbd53d/ty-0.0.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4e9ebb61529b9745af662e37c37a01ad743cdd2c95f0d1421705672874d806cd", size = 9630040, upload-time = "2025-12-20T21:19:38.165Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f8/6d1e87186e4c35eb64f28000c1df8fd5f73167ce126c5e3dd21fd1204a23/ty-0.0.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5eb191a8e332f50f56dfe45391bdd7d43dd4ef6e60884710fd7ce84c5d8c1eb5", size = 9754016, upload-time = "2025-12-20T21:19:32.79Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/20f989342cb3115852dda404f1d89a10a3ce93f14f42b23f095a3d1a00c9/ty-0.0.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:92ed7451a1e82ee134a2c24ca43b74dd31e946dff2b08e5c34473e6b051de542", size = 10252877, upload-time = "2025-12-20T21:19:20.787Z" }, + { url = "https://files.pythonhosted.org/packages/57/9d/fc66fa557443233dfad9ae197ff3deb70ae0efcfb71d11b30ef62f5cdcc3/ty-0.0.5-py3-none-win32.whl", hash = "sha256:71f6707e4c1c010c158029a688a498220f28bb22fdb6707e5c20e09f11a5e4f2", size = 9212640, upload-time = "2025-12-20T21:19:30.817Z" }, + { url = "https://files.pythonhosted.org/packages/68/b6/05c35f6dea29122e54af0e9f8dfedd0a100c721affc8cc801ebe2bc2ed13/ty-0.0.5-py3-none-win_amd64.whl", hash = "sha256:2b8b754a0d7191e94acdf0c322747fec34371a4d0669f5b4e89549aef28814ae", size = 10034701, upload-time = "2025-12-20T21:19:28.311Z" }, + { url = "https://files.pythonhosted.org/packages/df/ca/4201ed5cb2af73912663d0c6ded927c28c28b3c921c9348aa8d2cfef4853/ty-0.0.5-py3-none-win_arm64.whl", hash = "sha256:83bea5a5296caac20d52b790ded2b830a7ff91c4ed9f36730fe1f393ceed6654", size = 9566474, upload-time = "2025-12-20T21:19:22.518Z" }, +] + [[package]] name = "typing-extensions" version = "4.14.0"