diff --git a/.gitignore b/.gitignore index 2d020d366..e4ef2b4fe 100644 --- a/.gitignore +++ b/.gitignore @@ -429,3 +429,4 @@ code_to_optimize/**/package-lock.json # Other tools .codeflash/ +.codeflash_eval_worktrees/ diff --git a/codeflash/api/aiservice.py b/codeflash/api/aiservice.py index 3127649f2..9f66a998a 100644 --- a/codeflash/api/aiservice.py +++ b/codeflash/api/aiservice.py @@ -22,9 +22,9 @@ FunctionRepairInfo, OptimizationReviewResult, OptimizedCandidate, - OptimizedCandidateSource, TestFileReview, ) +from codeflash.models.shared_types import OptimizedCandidateSource from codeflash.telemetry.posthog_cf import ph from codeflash.version import __version__ as codeflash_version diff --git a/codeflash/cli_cmds/cli.py b/codeflash/cli_cmds/cli.py index 2db13efe8..b40777cf3 100644 --- a/codeflash/cli_cmds/cli.py +++ b/codeflash/cli_cmds/cli.py @@ -500,6 +500,13 @@ def _build_parser() -> ArgumentParser: ) parser.add_argument("--no-draft", default=False, action="store_true", help="Skip optimization for draft PRs") parser.add_argument("--worktree", default=False, action="store_true", help="Use worktree for optimization") + parser.add_argument( + "--parallel-candidates", + type=int, + default=0, + metavar="N", + help="Evaluate up to N optimization candidates in parallel using git worktrees (0 = sequential)", + ) parser.add_argument( "--testgen-review", default=False, action="store_true", help="Enable AI review and repair of generated tests" ) diff --git a/codeflash/code_utils/worktree_pool.py b/codeflash/code_utils/worktree_pool.py new file mode 100644 index 000000000..b4c40d24e --- /dev/null +++ b/codeflash/code_utils/worktree_pool.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import contextlib +import functools +import os +import shutil +import stat +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import anyio + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Self + +from codeflash.cli_cmds.console import logger +from codeflash.code_utils.git_utils import git_root_dir, mirror_path + + +class WorktreeSlot: + __slots__ = ("_git_root", "index", "path") + + def __init__(self, path: Path, index: int, git_root: Path) -> None: + self.path = path + self.index = index + self._git_root = git_root + + def mirror(self, original_path: Path) -> Path: + return mirror_path(original_path, self._git_root, self.path) + + async def write_candidate(self, file_path: Path, code: str) -> None: + mirrored = anyio.Path(self.mirror(file_path)) + await mirrored.parent.mkdir(parents=True, exist_ok=True) + await mirrored.write_text(code, encoding="utf-8") + + async def restore_file(self, file_path: Path, original_code: str) -> None: + mirrored = anyio.Path(self.mirror(file_path)) + await mirrored.write_text(original_code, encoding="utf-8") + + +class WorktreePool: + def __init__(self, pool_size: int = 4, base_dir: Path | None = None) -> None: + self._pool_size = pool_size + self._git_root = git_root_dir() + self._base_dir = base_dir or (self._git_root / ".codeflash_eval_worktrees") + self._slots: list[WorktreeSlot] = [] + self._send: anyio.abc.ObjectSendStream[WorktreeSlot] | None = None + self._receive: anyio.abc.ObjectReceiveStream[WorktreeSlot] | None = None + self._initialized = False + + async def initialize(self) -> None: + if self._initialized: + return + await anyio.Path(self._base_dir).mkdir(parents=True, exist_ok=True) + + async with anyio.create_task_group() as tg: + results: list[WorktreeSlot | None] = [None] * self._pool_size + for i in range(self._pool_size): + tg.start_soon(self._create_slot_task, i, results) + + self._slots = [s for s in results if s is not None] + self._send, self._receive = anyio.create_memory_object_stream[WorktreeSlot](self._pool_size) + for slot in self._slots: + await self._send.send(slot) + self._initialized = True + logger.debug(f"WorktreePool initialized with {len(self._slots)} slots at {self._base_dir}") + + async def _create_slot_task(self, index: int, results: list[WorktreeSlot | None]) -> None: + results[index] = await self._create_slot(index) + + async def _create_slot(self, index: int) -> WorktreeSlot: + slot_dir = self._base_dir / f"slot-{index}" + if slot_dir.exists(): + await anyio.to_thread.run_sync(functools.partial(shutil.rmtree, slot_dir, onerror=_handle_remove_readonly)) + + result = await anyio.run_process( + ["git", "-C", str(self._git_root), "worktree", "add", "--detach", str(slot_dir), "HEAD"], check=False + ) + if result.returncode != 0: + msg = f"Failed to create worktree slot {index}: {result.stderr.decode()}" + raise RuntimeError(msg) + + pid_file = anyio.Path(slot_dir / ".codeflash_pool.pid") + await pid_file.write_text(str(os.getpid()), encoding="utf-8") + + return WorktreeSlot(slot_dir, index, self._git_root) + + async def acquire(self) -> WorktreeSlot: + assert self._receive is not None + return await self._receive.receive() + + async def release(self, slot: WorktreeSlot) -> None: + assert self._send is not None + await self._send.send(slot) + + async def cleanup(self) -> None: + async with anyio.create_task_group() as tg: + for slot in self._slots: + tg.start_soon(self._remove_slot_async, slot) + self._slots.clear() + self._initialized = False + + if self._base_dir.exists(): + with contextlib.suppress(Exception): + await anyio.run_process(["git", "-C", str(self._git_root), "worktree", "prune"], check=False) + with contextlib.suppress(OSError): + self._base_dir.rmdir() + + async def _remove_slot_async(self, slot: WorktreeSlot) -> None: + if slot.path.exists(): + await anyio.to_thread.run_sync(functools.partial(shutil.rmtree, slot.path, onerror=_handle_remove_readonly)) + + async def __aenter__(self) -> Self: + await self.initialize() + return self + + async def __aexit__(self, *exc: object) -> None: + await self.cleanup() + + +def _handle_remove_readonly(func: Callable[..., Any], path: str, exc_info: tuple[Any, ...]) -> None: + if isinstance(exc_info[1], PermissionError): + Path(path).chmod(stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR) + func(path) + else: + raise exc_info[1] diff --git a/codeflash/languages/python/test_runner.py b/codeflash/languages/python/test_runner.py index 550f6bb05..d92ebdf2d 100644 --- a/codeflash/languages/python/test_runner.py +++ b/codeflash/languages/python/test_runner.py @@ -10,7 +10,6 @@ from codeflash.cli_cmds.console import logger from codeflash.code_utils.code_utils import custom_addopts -from codeflash.code_utils.shell_utils import get_cross_platform_subprocess_run_args from codeflash.languages.registry import get_language_support # Pattern to extract timing from stdout markers: !######...:######! @@ -92,11 +91,35 @@ def _ensure_runtime_files(project_root: Path, language: str = "javascript") -> N def execute_test_subprocess( cmd_list: list[str], cwd: Path, env: dict[str, str] | None, timeout: int = 600 -) -> subprocess.CompletedProcess: +) -> subprocess.CompletedProcess[str]: """Execute a subprocess with the given command list, working directory, environment variables, and timeout.""" logger.debug(f"executing test run with command: {' '.join(cmd_list)}") with custom_addopts(): - run_args = get_cross_platform_subprocess_run_args( - cwd=cwd, env=env, timeout=timeout, check=False, text=True, capture_output=True - ) - return subprocess.run(cmd_list, **run_args) # noqa: PLW1510 + return subprocess.run(cmd_list, cwd=cwd, env=env, timeout=timeout, check=False, text=True, capture_output=True) + + +async def async_execute_test_subprocess( + cmd_list: list[str], cwd: Path, env: dict[str, str] | None, timeout: int = 600 +) -> subprocess.CompletedProcess[str]: + """Execute a test subprocess asynchronously using anyio.""" + import os as _os + + import anyio + + logger.debug(f"async executing test run with command: {' '.join(cmd_list)}") + + merged_env = _os.environ.copy() + if env: + merged_env.update(env) + + with custom_addopts(): + try: + with anyio.fail_after(timeout): + result = await anyio.run_process(cmd_list, cwd=cwd, env=merged_env, check=False) + except TimeoutError as e: + raise subprocess.TimeoutExpired(cmd_list, timeout) from e + + stdout = result.stdout.decode("utf-8", errors="replace") if result.stdout else "" + stderr = result.stderr.decode("utf-8", errors="replace") if result.stderr else "" + + return subprocess.CompletedProcess(args=cmd_list, returncode=result.returncode, stdout=stdout, stderr=stderr) diff --git a/codeflash/models/function_types.py b/codeflash/models/function_types.py index bea6672b0..f5fa82bf4 100644 --- a/codeflash/models/function_types.py +++ b/codeflash/models/function_types.py @@ -12,14 +12,9 @@ from pydantic import Field from pydantic.dataclasses import dataclass +from codeflash.models.shared_types import FunctionParent -@dataclass(frozen=True) -class FunctionParent: - name: str - type: str - - def __str__(self) -> str: - return f"{self.type}:{self.name}" +__all__ = ["FunctionParent", "FunctionToOptimize"] @dataclass(frozen=True, config={"arbitrary_types_allowed": True}) diff --git a/codeflash/models/models.py b/codeflash/models/models.py index 640e5230a..33905d361 100644 --- a/codeflash/models/models.py +++ b/codeflash/models/models.py @@ -14,6 +14,7 @@ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, ValidationError, model_validator from pydantic.dataclasses import dataclass +from codeflash.models.shared_types import OptimizedCandidateSource from codeflash.models.test_type import TestType if TYPE_CHECKING: @@ -50,6 +51,23 @@ class AIServiceRefinerRequest: additional_context_files: dict[str, str] | None = None # {filepath: content} for imported modules +@dataclass(frozen=True) +class AIServiceBatchRefinerCandidate: + optimization_id: str + optimized_source_code: str + optimized_explanation: str + optimized_code_runtime: int + original_code_runtime: int + speedup: str + optimized_line_profiler_results: str + + +@dataclass(frozen=True) +class AIServiceBatchRefinerRequest: + shared_context: dict[str, Any] + candidates: list[dict[str, Any]] + + # this should be possible to auto serialize @dataclass(frozen=True) class AdaptiveOptimizedCandidate: @@ -298,11 +316,11 @@ def flat(self) -> str: """ if self._cache.get("flat") is not None: - return self._cache["flat"] + return cast("str", self._cache["flat"]) self._cache["flat"] = "\n".join( get_code_block_splitter(block.file_path) + "\n" + block.code for block in self.code_strings ) - return self._cache["flat"] + return cast("str", self._cache["flat"]) @property def markdown(self) -> str: @@ -332,7 +350,7 @@ def file_to_path(self) -> dict[str, str]: """ try: - return self._cache["file_to_path"] + return cast("dict[str, str]", self._cache["file_to_path"]) except KeyError: mapping = {str(code_string.file_path): code_string.code for code_string in self.code_strings} self._cache["file_to_path"] = mapping @@ -494,7 +512,7 @@ def _normalize_path_for_comparison(path: Path) -> str: # Only lowercase on Windows where filesystem is case-insensitive return resolved.lower() if sys.platform == "win32" else resolved - def __iter__(self) -> Iterator[TestFile]: + def __iter__(self) -> Iterator[TestFile]: # type: ignore[override] return iter(self.test_files) def __len__(self) -> int: @@ -514,9 +532,9 @@ class CandidateEvaluationContext: optimized_runtimes: dict[str, float | None] = Field(default_factory=dict) is_correct: dict[str, bool] = Field(default_factory=dict) optimized_line_profiler_results: dict[str, str] = Field(default_factory=dict) - ast_code_to_id: dict = Field(default_factory=dict) + ast_code_to_id: dict[str, Any] = Field(default_factory=dict) optimizations_post: dict[str, str] = Field(default_factory=dict) - valid_optimizations: list = Field(default_factory=list) + valid_optimizations: list[Any] = Field(default_factory=list) def record_failed_candidate(self, optimization_id: str) -> None: """Record results for a failed candidate.""" @@ -543,7 +561,7 @@ def handle_duplicate_candidate( # Copy results from the previous evaluation (use .get() in case past_opt_id was registered # but never benchmarked due to an unhandled exception in process_single_candidate) self.speedup_ratios[candidate.optimization_id] = self.speedup_ratios.get(past_opt_id) - self.is_correct[candidate.optimization_id] = self.is_correct.get(past_opt_id) + self.is_correct[candidate.optimization_id] = self.is_correct.get(past_opt_id, False) self.optimized_runtimes[candidate.optimization_id] = self.optimized_runtimes.get(past_opt_id) # Line profiler results only available for successful runs @@ -592,15 +610,6 @@ class TestsInFile: test_type: TestType -class OptimizedCandidateSource(str, Enum): - OPTIMIZE = "OPTIMIZE" - OPTIMIZE_LP = "OPTIMIZE_LP" - REFINE = "REFINE" - REPAIR = "REPAIR" - ADAPTIVE = "ADAPTIVE" - JIT_REWRITE = "JIT_REWRITE" - - @dataclass(frozen=True) class OptimizedCandidate: source_code: CodeStringsMarkdown @@ -631,7 +640,7 @@ class OriginalCodeBaseline(BaseModel): behavior_test_results: TestResults benchmarking_test_results: TestResults replay_benchmarking_test_results: Optional[dict[BenchmarkKey, TestResults]] = None - line_profile_results: dict + line_profile_results: dict[str, Any] runtime: int coverage_results: Optional[CoverageData] async_throughput: Optional[int] = None @@ -794,6 +803,7 @@ def get_src_code(self, test_path: Path) -> Optional[str]: ) if self.test_class_name: + assert self.test_function_name is not None for stmt in module_node.body: if isinstance(stmt, cst.ClassDef) and stmt.name.value == self.test_class_name: func_node = self.find_func_in_class(stmt, self.test_function_name) @@ -884,7 +894,7 @@ def group_by_benchmarks( """Group TestResults by benchmark for calculating improvements for each benchmark.""" from codeflash.code_utils.code_utils import module_name_from_file_path - test_results_by_benchmark = defaultdict(TestResults) + test_results_by_benchmark: defaultdict[BenchmarkKey, TestResults] = defaultdict(TestResults) benchmark_module_path = {} for benchmark_key in benchmark_keys: benchmark_module_path[benchmark_key] = module_name_from_file_path( @@ -1015,7 +1025,7 @@ def effective_loop_count(self) -> int: return max(loop_indices) if loop_indices else 0 def file_to_no_of_tests(self, test_functions_to_remove: list[str]) -> Counter[Path]: - map_gen_test_file_to_no_of_tests = Counter() + map_gen_test_file_to_no_of_tests: Counter[Path] = Counter() for gen_test_result in self.test_results: if ( gen_test_result.test_type == TestType.GENERATED_REGRESSION @@ -1024,7 +1034,7 @@ def file_to_no_of_tests(self, test_functions_to_remove: list[str]) -> Counter[Pa map_gen_test_file_to_no_of_tests[gen_test_result.file_name] += 1 return map_gen_test_file_to_no_of_tests - def __iter__(self) -> Iterator[FunctionTestInvocation]: + def __iter__(self) -> Iterator[FunctionTestInvocation]: # type: ignore[override] return iter(self.test_results) def __len__(self) -> int: @@ -1051,7 +1061,7 @@ def __eq__(self, other: object) -> bool: if len(self) != len(other): return False original_recursion_limit = sys.getrecursionlimit() - cast("TestResults", other) + assert isinstance(other, TestResults) for test_result in self: other_test_result = other.get_by_unique_invocation_loop_id(test_result.unique_invocation_loop_id) if other_test_result is None: diff --git a/codeflash/models/shared_types.py b/codeflash/models/shared_types.py new file mode 100644 index 000000000..4390b3d04 --- /dev/null +++ b/codeflash/models/shared_types.py @@ -0,0 +1,52 @@ +"""Shared types for cross-repo use between codeflash CLI and codeflash-internal server. + +This module defines types that are duplicated or shared between the client (CLI) +and the server. Centralizing them here allows both sides to import from a single +source of truth. +""" + +from __future__ import annotations + +from enum import Enum + +from pydantic.dataclasses import dataclass + +# --- Enums --- + + +class OptimizedCandidateSource(str, Enum): + OPTIMIZE = "OPTIMIZE" + OPTIMIZE_LP = "OPTIMIZE_LP" + REFINE = "REFINE" + REPAIR = "REPAIR" + ADAPTIVE = "ADAPTIVE" + JIT_REWRITE = "JIT_REWRITE" + + +# --- Models --- + + +@dataclass(frozen=True) +class FunctionParent: + name: str + type: str + + def __str__(self) -> str: + return f"{self.type}:{self.name}" + + +# --- Constants: Language identifiers --- + +LANGUAGE_PYTHON = "python" +LANGUAGE_JAVASCRIPT = "javascript" +LANGUAGE_TYPESCRIPT = "typescript" +LANGUAGE_JAVA = "java" + +SUPPORTED_LANGUAGES = frozenset({LANGUAGE_PYTHON, LANGUAGE_JAVASCRIPT, LANGUAGE_TYPESCRIPT, LANGUAGE_JAVA}) + +# --- Constants: Test type names --- + +TEST_TYPE_EXISTING_UNIT = "existing_unit_test" +TEST_TYPE_GENERATED_REGRESSION = "generated_regression" +TEST_TYPE_REPLAY = "replay_test" +TEST_TYPE_CONCOLIC_COVERAGE = "concolic_coverage_test" diff --git a/pyproject.toml b/pyproject.toml index 0a14b35e5..c37692d3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "tomlkit>=0.14.0", "attrs>=26.1.0", "requests>=2.32.5", + "anyio>=4.4.0", "junitparser>=4.0.2", "pydantic>=2.13.3", "humanize>=4.13.0", diff --git a/uv.lock b/uv.lock index 6a2b2a0f0..89cea85ae 100644 --- a/uv.lock +++ b/uv.lock @@ -55,6 +55,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/f9/f1c10e223c7b56a38109a3f2eb4e7fe9a757ea3ed3a166754fb30f65e466/ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec", size = 63675, upload-time = "2019-04-29T20:23:53.83Z" }, ] +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9.2' and python_full_version < '3.10'", + "python_full_version < '3.9.2'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + [[package]] name = "asttokens" version = "3.0.1" @@ -456,6 +506,8 @@ wheels = [ name = "codeflash" source = { editable = "." } dependencies = [ + { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "anyio", version = "4.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "attrs" }, { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, @@ -584,6 +636,7 @@ tests = [ [package.metadata] requires-dist = [ + { name = "anyio", specifier = ">=4.4.0" }, { name = "attrs", specifier = ">=26.1.0" }, { name = "click", specifier = ">=8.1.8" }, { name = "codeflash-benchmark", editable = "codeflash-benchmark" }, @@ -947,13 +1000,13 @@ version = "0.0.103" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "importlib-metadata", version = "9.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "packaging" }, - { name = "pygls" }, - { name = "typeshed-client" }, - { name = "typing-extensions" }, - { name = "typing-inspect" }, - { name = "z3-solver" }, + { name = "importlib-metadata", version = "9.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and python_full_version < '3.15'" }, + { name = "packaging", marker = "python_full_version < '3.15'" }, + { name = "pygls", marker = "python_full_version < '3.15'" }, + { name = "typeshed-client", marker = "python_full_version < '3.15'" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, + { name = "typing-inspect", marker = "python_full_version < '3.15'" }, + { name = "z3-solver", marker = "python_full_version < '3.15'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/28/56b5f1a4aa37d927c479012ae477acd67a5d14b4c6e4c65c1dcb33da99a0/crosshair_tool-0.0.103.tar.gz", hash = "sha256:02a2247ee79ba6d3b46e248199897539d8a26d4c5dc96821a12f34ebca715e81", size = 484767, upload-time = "2026-04-19T19:41:17.951Z" } wheels = [ @@ -1163,7 +1216,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1506,7 +1559,7 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "zipp", marker = "python_full_version >= '3.10'" }, + { name = "zipp", marker = "python_full_version >= '3.10' and python_full_version < '3.15'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } wheels = [ @@ -2428,7 +2481,7 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "uc-micro-py", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "uc-micro-py", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and sys_platform != 'win32') or (python_full_version == '3.10.*' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } wheels = [ @@ -2727,7 +2780,7 @@ wheels = [ [package.optional-dependencies] linkify = [ - { name = "linkify-it-py", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "linkify-it-py", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and sys_platform != 'win32') or (python_full_version == '3.10.*' and sys_platform == 'win32')" }, ] [[package]] @@ -2872,7 +2925,7 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and sys_platform != 'win32') or (python_full_version == '3.10.*' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } wheels = [ @@ -2893,9 +2946,9 @@ name = "memray" version = "1.19.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jinja2" }, - { name = "rich" }, - { name = "textual" }, + { name = "jinja2", marker = "python_full_version < '3.11' or sys_platform != 'win32'" }, + { name = "rich", marker = "python_full_version < '3.11' or sys_platform != 'win32'" }, + { name = "textual", marker = "python_full_version < '3.11' or sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/04/5b886a36df947599e0f37cd46e6e44e565299815f044e2303ab2ae9f8870/memray-1.19.3.tar.gz", hash = "sha256:4e0fb29ff0a50c0ec9dc84294d8f2c83419feba561a37628b304c2ae4fe73d03", size = 2417089, upload-time = "2026-04-08T18:49:32.409Z" } wheels = [ @@ -4145,7 +4198,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -4785,9 +4838,9 @@ name = "pytest-memray" version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "memray" }, + { name = "memray", marker = "python_full_version < '3.11' or sys_platform != 'win32'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and sys_platform != 'win32') or (python_full_version == '3.10.*' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/28/f67963efed56d847d028d0bb939f26cdeb32c4de474b3befc9da43bf18f9/pytest_memray-1.8.0.tar.gz", hash = "sha256:c0c706ef81941a7aa7064f2b3b8b5cdc0cea72b5277c6a6a09b113ca9ab30bdb", size = 240608, upload-time = "2025-08-18T17:32:47.329Z" } wheels = [ @@ -5785,14 +5838,14 @@ version = "8.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, extra = ["linkify"], marker = "python_full_version < '3.10'" }, - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, extra = ["linkify"], marker = "python_full_version >= '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, extra = ["linkify"], marker = "(python_full_version >= '3.10' and sys_platform != 'win32') or (python_full_version == '3.10.*' and sys_platform == 'win32')" }, { name = "mdit-py-plugins", version = "0.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "mdit-py-plugins", version = "0.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mdit-py-plugins", version = "0.5.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and sys_platform != 'win32') or (python_full_version == '3.10.*' and sys_platform == 'win32')" }, { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "platformdirs", version = "4.9.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pygments" }, - { name = "rich" }, - { name = "typing-extensions" }, + { name = "platformdirs", version = "4.9.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and sys_platform != 'win32') or (python_full_version == '3.10.*' and sys_platform == 'win32')" }, + { name = "pygments", marker = "python_full_version < '3.11' or sys_platform != 'win32'" }, + { name = "rich", marker = "python_full_version < '3.11' or sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/89/bec5709fb759f9c784bbcb30b2e3497df3f901691d13c2b864dbf6694a17/textual-8.2.4.tar.gz", hash = "sha256:d4e2b2ddd7157191d00b228592b7c739ea080b7d792fd410f23ca75f05ea76c4", size = 1848933, upload-time = "2026-04-19T04:20:45.845Z" } wheels = [ @@ -7057,8 +7110,8 @@ version = "2.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-resources", version = "6.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "importlib-resources", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions" }, + { name = "importlib-resources", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and python_full_version < '3.15'" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/de/cc79c33f6268740567ea109c11809e3c799daf5c1c5aeffb9c3f3b052dbe/typeshed_client-2.10.0.tar.gz", hash = "sha256:906bf343595aed4a120ccc0a35dde2d85cae8c15d015703a768541291e38cfc3", size = 522565, upload-time = "2026-04-18T04:27:36.234Z" } wheels = [ @@ -7079,8 +7132,8 @@ name = "typing-inspect" version = "0.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mypy-extensions" }, - { name = "typing-extensions" }, + { name = "mypy-extensions", marker = "python_full_version < '3.15'" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } wheels = [