comfy-env 0.0.8__py3-none-any.whl → 0.0.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
comfy_env/__init__.py CHANGED
@@ -40,7 +40,7 @@ This package provides:
40
40
  The @isolated decorator and WorkerBridge are still available.
41
41
  """
42
42
 
43
- __version__ = "0.0.8"
43
+ __version__ = "0.0.11"
44
44
 
45
45
  from .env.config import IsolatedEnv, EnvManagerConfig, LocalConfig, NodeReq
46
46
  from .env.config_file import (
comfy_env/cli.py CHANGED
@@ -234,6 +234,7 @@ def cmd_info(args) -> int:
234
234
  def cmd_resolve(args) -> int:
235
235
  """Handle resolve command."""
236
236
  from .resolver import RuntimeEnv, WheelResolver, parse_wheel_requirement
237
+ from .registry import PACKAGE_REGISTRY
237
238
  from .env.config_file import discover_env_config, load_env_from_file
238
239
 
239
240
  env = RuntimeEnv.detect()
@@ -270,19 +271,116 @@ def cmd_resolve(args) -> int:
270
271
  print(f" {package}: No version specified, skipping")
271
272
  continue
272
273
 
274
+ pkg_lower = package.lower()
273
275
  try:
274
- url = resolver.resolve(package, version, env, verify=args.verify)
275
- status = "OK" if args.verify else "resolved"
276
- print(f" {package}=={version}: {status}")
277
- print(f" {url}")
276
+ # Check if package is in registry with github_release method
277
+ if pkg_lower in PACKAGE_REGISTRY:
278
+ registry_config = PACKAGE_REGISTRY[pkg_lower]
279
+ method = registry_config.get("method")
280
+
281
+ if method == "github_release":
282
+ # Resolve URL from registry sources
283
+ url = _resolve_github_release_url(package, version, env, registry_config)
284
+ status = "OK" if args.verify else "resolved"
285
+ print(f" {package}=={version}: {status}")
286
+ print(f" {url}")
287
+ else:
288
+ # For other methods, just show what method will be used
289
+ print(f" {package}=={version}: uses {method} method")
290
+ if "index_url" in registry_config:
291
+ index_url = _substitute_template(registry_config["index_url"], env)
292
+ print(f" index: {index_url}")
293
+ elif "package_template" in registry_config:
294
+ pkg_name = _substitute_template(registry_config["package_template"], env)
295
+ print(f" installs as: {pkg_name}")
296
+ else:
297
+ # Fall back to WheelResolver
298
+ url = resolver.resolve(package, version, env, verify=args.verify)
299
+ status = "OK" if args.verify else "resolved"
300
+ print(f" {package}=={version}: {status}")
301
+ print(f" {url}")
278
302
  except Exception as e:
279
303
  print(f" {package}=={version}: FAILED")
280
- print(f" {e}")
304
+ _print_wheel_not_found_error(package, version, env, e)
281
305
  all_ok = False
282
306
 
283
307
  return 0 if all_ok else 1
284
308
 
285
309
 
310
+ def _substitute_template(template: str, env) -> str:
311
+ """Substitute environment variables into a URL template."""
312
+ vars_dict = env.as_dict()
313
+ result = template
314
+ for key, value in vars_dict.items():
315
+ if value is not None:
316
+ result = result.replace(f"{{{key}}}", str(value))
317
+ return result
318
+
319
+
320
+ def _resolve_github_release_url(package: str, version: str, env, config: dict) -> str:
321
+ """Resolve URL for github_release method packages."""
322
+ sources = config.get("sources", [])
323
+ if not sources:
324
+ raise ValueError(f"No sources configured for {package}")
325
+
326
+ # Build template variables
327
+ vars_dict = env.as_dict()
328
+ vars_dict["version"] = version
329
+ vars_dict["py_tag"] = f"cp{env.python_short}"
330
+ if env.cuda_version:
331
+ vars_dict["cuda_major"] = env.cuda_version.split(".")[0]
332
+
333
+ # Filter sources by platform
334
+ current_platform = env.platform_tag
335
+ compatible_sources = [
336
+ s for s in sources
337
+ if current_platform in s.get("platforms", [])
338
+ ]
339
+
340
+ if not compatible_sources:
341
+ available = set()
342
+ for s in sources:
343
+ available.update(s.get("platforms", []))
344
+ raise ValueError(
345
+ f"No {package} wheels for platform {current_platform}. "
346
+ f"Available: {', '.join(sorted(available))}"
347
+ )
348
+
349
+ # Return URL from first compatible source
350
+ source = compatible_sources[0]
351
+ url_template = source.get("url_template", "")
352
+ url = url_template
353
+ for key, value in vars_dict.items():
354
+ if value is not None:
355
+ url = url.replace(f"{{{key}}}", str(value))
356
+
357
+ return url
358
+
359
+
360
+ def _print_wheel_not_found_error(package: str, version: str, env, error: Exception) -> None:
361
+ """Print a formatted error message for wheel not found."""
362
+ from .errors import WheelNotFoundError
363
+
364
+ if isinstance(error, WheelNotFoundError):
365
+ print(f" CUDA wheel not found: {package}=={version}")
366
+ print()
367
+ print("+------------------------------------------------------------------+")
368
+ print("| CUDA Wheel Not Found |")
369
+ print("+------------------------------------------------------------------+")
370
+ print(f"| Package: {package}=={version:<46} |")
371
+ print(f"| Requested: cu{env.cuda_short}-torch{env.torch_mm}-{env.python_short}-{env.platform_tag:<17} |")
372
+ print("| |")
373
+ print(f"| Reason: {error.reason:<54} |")
374
+ print("| |")
375
+ print("| Suggestions: |")
376
+ print(f"| 1. Check if wheel exists: comfy-env resolve {package:<15} |")
377
+ print(f"| 2. Build wheel locally: comfy-env build {package:<18} |")
378
+ print("| |")
379
+ print("+------------------------------------------------------------------+")
380
+ else:
381
+ print(f" {error}")
382
+
383
+
286
384
  def cmd_doctor(args) -> int:
287
385
  """Handle doctor command."""
288
386
  from .install import verify_installation
comfy_env/env/config.py CHANGED
@@ -98,6 +98,10 @@ class IsolatedEnv:
98
98
  cuda: Optional[str] = None
99
99
  requirements: list[str] = field(default_factory=list)
100
100
  no_deps_requirements: list[str] = field(default_factory=list) # Install with --no-deps
101
+ # Platform-specific requirements (merged at install time)
102
+ windows_requirements: list[str] = field(default_factory=list)
103
+ linux_requirements: list[str] = field(default_factory=list)
104
+ darwin_requirements: list[str] = field(default_factory=list)
101
105
  requirements_file: Optional[Path] = None
102
106
  wheel_sources: list[str] = field(default_factory=list)
103
107
  index_urls: list[str] = field(default_factory=list)
@@ -168,7 +168,7 @@ def _get_default_pytorch_version(cuda_version: Optional[str]) -> str:
168
168
  - CUDA 12.8 (Turing+): PyTorch 2.8.0
169
169
  """
170
170
  if cuda_version == "12.4":
171
- return "2.5.1" # Legacy: Pascal GPUs
171
+ return "2.5.1" # Full: Pascal GPUs
172
172
  return "2.8.0" # Modern: Turing through Blackwell
173
173
 
174
174
 
@@ -176,7 +176,7 @@ def _parse_config(data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
176
176
  """
177
177
  Parse TOML data into IsolatedEnv.
178
178
 
179
- Supports both simplified and legacy config formats:
179
+ Supports both simplified and full config formats:
180
180
 
181
181
  Simplified (CUDA packages only):
182
182
  [packages]
@@ -186,7 +186,7 @@ def _parse_config(data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
186
186
  Or as list:
187
187
  packages = ["torch-scatter==2.1.2", "torch-cluster==1.6.3"]
188
188
 
189
- Legacy:
189
+ Full:
190
190
  [env]
191
191
  name = "my-node"
192
192
  [packages]
@@ -233,7 +233,7 @@ def _parse_config(data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
233
233
  variables.setdefault("pytorch_mm", pytorch_mm)
234
234
 
235
235
  # Parse CUDA packages - support multiple formats
236
- # Priority: [cuda] section > cuda = [...] > legacy [packages] section
236
+ # Priority: [cuda] section > cuda = [...] > [packages] section
237
237
  no_deps_requirements = []
238
238
  requirements = []
239
239
 
@@ -258,12 +258,12 @@ def _parse_config(data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
258
258
 
259
259
  elif isinstance(packages_section, dict):
260
260
  # Check for simplified format: [packages] with key=value pairs
261
- # vs legacy format: [packages] with requirements/no_deps lists
261
+ # vs old format: [packages] with requirements/no_deps lists
262
262
 
263
- has_legacy_keys = any(k in packages_section for k in ["requirements", "no_deps", "requirements_file"])
263
+ has_old_keys = any(k in packages_section for k in ["requirements", "no_deps", "requirements_file"])
264
264
 
265
- if has_legacy_keys:
266
- # Legacy format
265
+ if has_old_keys:
266
+ # Old format
267
267
  raw_requirements = packages_section.get("requirements", [])
268
268
  requirements = [_substitute_vars(req, variables) for req in raw_requirements]
269
269
 
@@ -340,9 +340,9 @@ def load_config(
340
340
  base_dir: Optional[Path] = None,
341
341
  ) -> EnvManagerConfig:
342
342
  """
343
- Load full EnvManagerConfig from a TOML file (v2 schema).
343
+ Load full EnvManagerConfig from a TOML file.
344
344
 
345
- Supports both v2 schema and legacy format (auto-detected).
345
+ Supports both full schema (named envs) and simple format (auto-detected).
346
346
 
347
347
  Args:
348
348
  path: Path to the TOML config file
@@ -373,7 +373,7 @@ def load_config(
373
373
  with open(path, "rb") as f:
374
374
  data = tomllib.load(f)
375
375
 
376
- return _parse_config_v2(data, base_dir)
376
+ return _parse_full_config(data, base_dir)
377
377
 
378
378
 
379
379
  def discover_config(
@@ -404,9 +404,9 @@ def discover_config(
404
404
  return None
405
405
 
406
406
 
407
- def _parse_config_v2(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
407
+ def _parse_full_config(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
408
408
  """
409
- Parse TOML data into EnvManagerConfig (v2 schema).
409
+ Parse TOML data into EnvManagerConfig.
410
410
 
411
411
  Schema:
412
412
  [local.cuda] - CUDA packages for host
@@ -416,16 +416,16 @@ def _parse_config_v2(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
416
416
  [envname.packages] - Packages for env
417
417
  [node_reqs] - Node dependencies
418
418
 
419
- Also supports legacy format for backward compatibility.
419
+ Also supports simple format ([env] + [packages]) for backward compatibility.
420
420
  """
421
- # Detect if this is v2 schema or legacy
422
- is_v2 = "local" in data or _has_named_env(data)
421
+ # Detect if this is full schema or simple format
422
+ is_full = "local" in data or _has_named_env(data)
423
423
 
424
- if not is_v2:
425
- # Legacy format - convert to v2 structure
426
- return _convert_legacy_to_v2(data, base_dir)
424
+ if not is_full:
425
+ # Simple format - convert to full structure
426
+ return _convert_simple_to_full(data, base_dir)
427
427
 
428
- # Parse v2 schema
428
+ # Parse full schema
429
429
  local = _parse_local_section(data.get("local", {}))
430
430
  envs = _parse_env_sections(data, base_dir)
431
431
  node_reqs = _parse_node_reqs(data.get("node_reqs", {}))
@@ -529,6 +529,30 @@ def _parse_single_env(name: str, env_data: Dict[str, Any], base_dir: Path) -> Is
529
529
  elif isinstance(packages_section, list):
530
530
  requirements = packages_section
531
531
 
532
+ # Parse platform-specific packages [envname.packages.windows], etc.
533
+ windows_reqs = []
534
+ linux_reqs = []
535
+ darwin_reqs = []
536
+
537
+ if isinstance(packages_section, dict):
538
+ win_section = packages_section.get("windows", {})
539
+ if isinstance(win_section, dict):
540
+ windows_reqs = win_section.get("requirements", [])
541
+ elif isinstance(win_section, list):
542
+ windows_reqs = win_section
543
+
544
+ linux_section = packages_section.get("linux", {})
545
+ if isinstance(linux_section, dict):
546
+ linux_reqs = linux_section.get("requirements", [])
547
+ elif isinstance(linux_section, list):
548
+ linux_reqs = linux_section
549
+
550
+ darwin_section = packages_section.get("darwin", {})
551
+ if isinstance(darwin_section, dict):
552
+ darwin_reqs = darwin_section.get("requirements", [])
553
+ elif isinstance(darwin_section, list):
554
+ darwin_reqs = darwin_section
555
+
532
556
  return IsolatedEnv(
533
557
  name=name,
534
558
  python=python,
@@ -536,6 +560,9 @@ def _parse_single_env(name: str, env_data: Dict[str, Any], base_dir: Path) -> Is
536
560
  pytorch_version=pytorch,
537
561
  requirements=requirements,
538
562
  no_deps_requirements=no_deps_requirements,
563
+ windows_requirements=windows_reqs,
564
+ linux_requirements=linux_reqs,
565
+ darwin_requirements=darwin_reqs,
539
566
  )
540
567
 
541
568
 
@@ -555,27 +582,30 @@ def _parse_node_reqs(node_reqs_data: Dict[str, Any]) -> List[NodeReq]:
555
582
  return reqs
556
583
 
557
584
 
558
- def _convert_legacy_to_v2(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
559
- """Convert legacy config format to v2 EnvManagerConfig."""
560
- # Parse using legacy parser to get IsolatedEnv
561
- legacy_env = _parse_config(data, base_dir)
585
+ def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
586
+ """Convert simple config format to full EnvManagerConfig.
562
587
 
563
- # Check if this is really just a Type 2 config (local CUDA only, no venv)
564
- # Type 2 indicators: no [env] section with name, or name matches directory
588
+ Simple configs have [env] and [packages] sections but no named environments.
589
+ This converts them to the full format with a single named environment.
590
+ """
591
+ # Parse using simple parser to get IsolatedEnv
592
+ simple_env = _parse_config(data, base_dir)
593
+
594
+ # Check if this has explicit env settings (isolated venv) vs just CUDA packages (local install)
565
595
  env_section = data.get("env", {})
566
596
  has_explicit_env = bool(env_section.get("name") or env_section.get("python"))
567
597
 
568
598
  if has_explicit_env:
569
- # This is a Type 1 config (isolated venv)
599
+ # Isolated venv config
570
600
  return EnvManagerConfig(
571
601
  local=LocalConfig(),
572
- envs={legacy_env.name: legacy_env},
602
+ envs={simple_env.name: simple_env},
573
603
  node_reqs=[],
574
604
  )
575
605
  else:
576
- # This is a Type 2 config (local CUDA only)
606
+ # Local CUDA packages only (no isolated venv)
577
607
  cuda_packages = {}
578
- for req in legacy_env.no_deps_requirements:
608
+ for req in simple_env.no_deps_requirements:
579
609
  if "==" in req:
580
610
  pkg, ver = req.split("==", 1)
581
611
  cuda_packages[pkg] = ver
@@ -585,8 +615,8 @@ def _convert_legacy_to_v2(data: Dict[str, Any], base_dir: Path) -> EnvManagerCon
585
615
  return EnvManagerConfig(
586
616
  local=LocalConfig(
587
617
  cuda_packages=cuda_packages,
588
- requirements=legacy_env.requirements,
618
+ requirements=simple_env.requirements,
589
619
  ),
590
620
  envs={},
591
621
  node_reqs=[],
592
- )
622
+ )
@@ -7,10 +7,16 @@ vs older GPUs which use CUDA 12.4.
7
7
  This runs BEFORE PyTorch is installed, so we use nvidia-smi directly.
8
8
  """
9
9
 
10
+ import os
10
11
  import subprocess
11
12
  from typing import List, Dict, Optional
12
13
 
13
14
 
15
+ # Environment variable to override CUDA version detection
16
+ # Useful for CI environments without GPU
17
+ CUDA_VERSION_ENV_VAR = "COMFY_ENV_CUDA_VERSION"
18
+
19
+
14
20
  def detect_gpu_info() -> List[Dict[str, str]]:
15
21
  """
16
22
  Detect GPU name and compute capability using nvidia-smi.
@@ -86,22 +92,6 @@ def is_blackwell_gpu(name: str, compute_cap: str) -> bool:
86
92
  return False
87
93
 
88
94
 
89
- def needs_cuda_128() -> bool:
90
- """
91
- Check if any detected GPU requires CUDA 12.8.
92
-
93
- Returns:
94
- True if Blackwell GPU detected, False otherwise.
95
- """
96
- gpus = detect_gpu_info()
97
-
98
- for gpu in gpus:
99
- if is_blackwell_gpu(gpu["name"], gpu["compute_cap"]):
100
- return True
101
-
102
- return False
103
-
104
-
105
95
  def is_legacy_gpu(compute_cap: str) -> bool:
106
96
  """
107
97
  Check if GPU is Pascal or older (requires legacy CUDA/PyTorch).
@@ -126,7 +116,12 @@ def detect_cuda_version() -> Optional[str]:
126
116
  Returns:
127
117
  "12.4" for Pascal or older (compute < 7.5),
128
118
  "12.8" for Turing or newer (compute >= 7.5),
129
- None if no GPU detected.
119
+ None if no GPU detected and no env var override.
120
+
121
+ Environment Variable Override:
122
+ Set COMFY_ENV_CUDA_VERSION to override auto-detection.
123
+ Useful for CI environments without GPU.
124
+ Example: COMFY_ENV_CUDA_VERSION=12.8
130
125
 
131
126
  GPU Architecture Reference:
132
127
  - Pascal (GTX 10xx, P100): compute 6.0-6.1 → CUDA 12.4
@@ -138,6 +133,15 @@ def detect_cuda_version() -> Optional[str]:
138
133
  """
139
134
  gpus = detect_gpu_info()
140
135
  if not gpus:
136
+ # No GPU detected - check for env var override
137
+ override = os.environ.get(CUDA_VERSION_ENV_VAR)
138
+ if override:
139
+ # Normalize format (e.g., "12.8" or "128" -> "12.8")
140
+ override = override.strip()
141
+ if override and '.' not in override and len(override) >= 2:
142
+ # Convert "128" -> "12.8"
143
+ override = f"{override[:-1]}.{override[-1]}"
144
+ return override
141
145
  return None
142
146
 
143
147
  # Check if any GPU is legacy (Pascal or older)
@@ -159,7 +163,10 @@ def get_gpu_summary() -> str:
159
163
  gpus = detect_gpu_info()
160
164
 
161
165
  if not gpus:
162
- return "No NVIDIA GPU detected"
166
+ override = os.environ.get(CUDA_VERSION_ENV_VAR)
167
+ if override:
168
+ return f"No NVIDIA GPU detected (using {CUDA_VERSION_ENV_VAR}={override})"
169
+ return f"No NVIDIA GPU detected (set {CUDA_VERSION_ENV_VAR} to override)"
163
170
 
164
171
  lines = []
165
172
  for i, gpu in enumerate(gpus):
comfy_env/env/manager.py CHANGED
@@ -6,6 +6,7 @@ Uses uv for fast environment creation and package installation.
6
6
 
7
7
  import subprocess
8
8
  import shutil
9
+ import sys
9
10
  from pathlib import Path
10
11
  from typing import Optional, Callable
11
12
 
@@ -263,13 +264,29 @@ class IsolatedEnvManager:
263
264
  """
264
265
  python_exe = self.get_python(env)
265
266
 
266
- if not env.requirements and not env.requirements_file:
267
+ # Merge platform-specific requirements
268
+ if sys.platform == 'win32':
269
+ platform_reqs = env.windows_requirements
270
+ platform_name = 'Windows'
271
+ elif sys.platform == 'darwin':
272
+ platform_reqs = env.darwin_requirements
273
+ platform_name = 'macOS'
274
+ else:
275
+ platform_reqs = env.linux_requirements
276
+ platform_name = 'Linux'
277
+
278
+ all_requirements = list(env.requirements) + list(platform_reqs)
279
+
280
+ if platform_reqs:
281
+ self.log(f"Including {len(platform_reqs)} {platform_name}-specific packages")
282
+
283
+ if not all_requirements and not env.requirements_file:
267
284
  self.log("No requirements to install")
268
285
  return
269
286
 
270
287
  # Validate requirements for security
271
- if env.requirements:
272
- validate_dependencies(env.requirements)
288
+ if all_requirements:
289
+ validate_dependencies(all_requirements)
273
290
 
274
291
  # Validate wheel sources
275
292
  for wheel_source in env.wheel_sources:
@@ -311,11 +328,11 @@ class IsolatedEnvManager:
311
328
  self.log(f"Installing {len(env.no_deps_requirements)} CUDA packages")
312
329
  self._install_cuda_packages(env, pip_args)
313
330
 
314
- # Install individual requirements
315
- if env.requirements:
316
- self.log(f"Installing {len(env.requirements)} packages")
331
+ # Install individual requirements (including platform-specific)
332
+ if all_requirements:
333
+ self.log(f"Installing {len(all_requirements)} packages")
317
334
  result = subprocess.run(
318
- pip_args + env.requirements,
335
+ pip_args + all_requirements,
319
336
  capture_output=True,
320
337
  text=True,
321
338
  )
@@ -458,7 +475,7 @@ class IsolatedEnvManager:
458
475
  url_template = source["url_template"]
459
476
  url = self._substitute_template(url_template, vars_dict)
460
477
 
461
- self.log(f" Trying {source.get('name', 'unknown')}: {url[:80]}...")
478
+ self.log(f" Trying {source.get('name', 'unknown')}: {url}")
462
479
  result = subprocess.run(
463
480
  pip_args + ["--no-deps", url],
464
481
  capture_output=True, text=True,
@@ -467,7 +484,7 @@ class IsolatedEnvManager:
467
484
  if result.returncode == 0:
468
485
  return # Success!
469
486
 
470
- errors.append(f"{source.get('name', 'unknown')}: {result.stderr[:100]}")
487
+ errors.append(f"{source.get('name', 'unknown')}: {result.stderr.strip()}")
471
488
 
472
489
  # All sources failed
473
490
  raise RuntimeError(
comfy_env/errors.py CHANGED
@@ -291,35 +291,3 @@ class InstallError(EnvManagerError):
291
291
 
292
292
  details = "\n".join(details_parts) if details_parts else None
293
293
  super().__init__(message, details)
294
-
295
-
296
- def format_environment_mismatch(
297
- expected: "RuntimeEnv",
298
- actual: "RuntimeEnv",
299
- ) -> str:
300
- """
301
- Format a message explaining environment mismatch.
302
-
303
- Used when the current environment doesn't match what's needed.
304
- """
305
- mismatches = []
306
-
307
- if expected.cuda_version != actual.cuda_version:
308
- mismatches.append(
309
- f" CUDA: expected {expected.cuda_version}, got {actual.cuda_version}"
310
- )
311
-
312
- if expected.torch_version != actual.torch_version:
313
- mismatches.append(
314
- f" PyTorch: expected {expected.torch_version}, got {actual.torch_version}"
315
- )
316
-
317
- if expected.python_version != actual.python_version:
318
- mismatches.append(
319
- f" Python: expected {expected.python_version}, got {actual.python_version}"
320
- )
321
-
322
- if not mismatches:
323
- return "Environment matches expected configuration"
324
-
325
- return "Environment mismatch:\n" + "\n".join(mismatches)
comfy_env/install.py CHANGED
@@ -421,6 +421,7 @@ def _install_from_github_release(
421
421
  url = url.replace(f"{{{key}}}", str(value))
422
422
 
423
423
  log(f" Trying {source_name}: {package}=={version}...")
424
+ log(f" Resolved wheel to: {url}")
424
425
 
425
426
  try:
426
427
  pip_cmd = _get_pip_command()
comfy_env/ipc/bridge.py CHANGED
@@ -48,10 +48,6 @@ class WorkerBridge:
48
48
  result = bridge.call("process", image=my_image)
49
49
  """
50
50
 
51
- # Singleton instances by environment hash
52
- _instances: Dict[str, "WorkerBridge"] = {}
53
- _instances_lock = threading.Lock()
54
-
55
51
  def __init__(
56
52
  self,
57
53
  env: IsolatedEnv,
@@ -81,38 +77,6 @@ class WorkerBridge:
81
77
  self._process_lock = threading.Lock()
82
78
  self._stderr_thread: Optional[threading.Thread] = None
83
79
 
84
- @classmethod
85
- def get_instance(
86
- cls,
87
- env: IsolatedEnv,
88
- worker_script: Path,
89
- base_dir: Optional[Path] = None,
90
- log_callback: Optional[Callable[[str], None]] = None,
91
- ) -> "WorkerBridge":
92
- """
93
- Get or create a singleton bridge instance for an environment.
94
-
95
- Args:
96
- env: Isolated environment configuration
97
- worker_script: Path to the worker Python script
98
- base_dir: Base directory for environments
99
- log_callback: Optional callback for logging
100
-
101
- Returns:
102
- WorkerBridge instance (reused if same env hash)
103
- """
104
- env_hash = env.get_env_hash()
105
-
106
- with cls._instances_lock:
107
- if env_hash not in cls._instances:
108
- cls._instances[env_hash] = cls(
109
- env=env,
110
- worker_script=worker_script,
111
- base_dir=base_dir,
112
- log_callback=log_callback,
113
- )
114
- return cls._instances[env_hash]
115
-
116
80
  @classmethod
117
81
  def from_config_file(
118
82
  cls,