comfy-env 0.0.49__py3-none-any.whl → 0.0.51__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.
@@ -0,0 +1,310 @@
1
+ """
2
+ Import stub system for isolated node packs.
3
+
4
+ This module provides automatic import stubbing for packages that exist only
5
+ in the isolated pixi environment, not in the host ComfyUI Python.
6
+
7
+ How it works:
8
+ 1. Read package names from comfy-env.toml
9
+ 2. Look up their import names from top_level.txt in the pixi environment
10
+ 3. Register import hooks that provide stub modules for those imports
11
+ 4. Stubs allow class definitions to parse without the real packages
12
+ 5. Real packages are used when FUNCTION runs in the isolated worker
13
+
14
+ Usage:
15
+ # In node pack's __init__.py, BEFORE importing nodes:
16
+ from comfy_env import setup_isolated_imports
17
+ setup_isolated_imports(__file__)
18
+
19
+ from .nodes import NODE_CLASS_MAPPINGS # Now works!
20
+ """
21
+
22
+ import sys
23
+ import types
24
+ from pathlib import Path
25
+ from typing import Dict, List, Optional, Set
26
+
27
+
28
+ class _StubModule(types.ModuleType):
29
+ """
30
+ A stub module that accepts any attribute access or call.
31
+ """
32
+
33
+ def __init__(self, name: str):
34
+ super().__init__(name)
35
+ self.__path__ = [] # Make it a package
36
+ self.__file__ = f"<stub:{name}>"
37
+ self._stub_name = name
38
+
39
+ def __getattr__(self, name: str):
40
+ if name.startswith('_'):
41
+ raise AttributeError(name)
42
+ return _StubObject(f"{self._stub_name}.{name}")
43
+
44
+ def __repr__(self):
45
+ return f"<StubModule '{self._stub_name}'>"
46
+
47
+
48
+ class _StubObject:
49
+ """
50
+ A stub object that accepts any operation.
51
+ """
52
+
53
+ def __init__(self, name: str = "stub"):
54
+ self._stub_name = name
55
+
56
+ def __getattr__(self, name: str):
57
+ if name.startswith('_'):
58
+ raise AttributeError(name)
59
+ return _StubObject(f"{self._stub_name}.{name}")
60
+
61
+ def __call__(self, *args, **kwargs):
62
+ return _StubObject(f"{self._stub_name}()")
63
+
64
+ def __iter__(self):
65
+ return iter([])
66
+
67
+ def __len__(self):
68
+ return 0
69
+
70
+ def __bool__(self):
71
+ return False
72
+
73
+ def __enter__(self):
74
+ return self
75
+
76
+ def __exit__(self, *args):
77
+ pass
78
+
79
+ def __repr__(self):
80
+ return f"<Stub '{self._stub_name}'>"
81
+
82
+ def __add__(self, other): return self
83
+ def __radd__(self, other): return self
84
+ def __sub__(self, other): return self
85
+ def __rsub__(self, other): return self
86
+ def __mul__(self, other): return self
87
+ def __rmul__(self, other): return self
88
+ def __truediv__(self, other): return self
89
+ def __rtruediv__(self, other): return self
90
+ def __eq__(self, other): return False
91
+ def __ne__(self, other): return True
92
+ def __lt__(self, other): return False
93
+ def __le__(self, other): return False
94
+ def __gt__(self, other): return False
95
+ def __ge__(self, other): return False
96
+ def __hash__(self): return hash(self._stub_name)
97
+ def __getitem__(self, key): return _StubObject(f"{self._stub_name}[{key}]")
98
+ def __setitem__(self, key, value): pass
99
+ def __contains__(self, item): return False
100
+
101
+
102
+ class _StubFinder:
103
+ """Import hook finder that provides stub modules for specified packages."""
104
+
105
+ def __init__(self, stub_packages: Set[str]):
106
+ self.stub_packages = stub_packages
107
+
108
+ def find_module(self, fullname: str, path=None):
109
+ top_level = fullname.split('.')[0]
110
+ if top_level in self.stub_packages:
111
+ return _StubLoader(self.stub_packages)
112
+ return None
113
+
114
+
115
+ class _StubLoader:
116
+ """Import hook loader that creates stub modules."""
117
+
118
+ def __init__(self, stub_packages: Set[str]):
119
+ self.stub_packages = stub_packages
120
+
121
+ def load_module(self, fullname: str):
122
+ if fullname in sys.modules:
123
+ return sys.modules[fullname]
124
+
125
+ module = _StubModule(fullname)
126
+ module.__loader__ = self
127
+
128
+ if '.' in fullname:
129
+ parent = fullname.rsplit('.', 1)[0]
130
+ module.__package__ = parent
131
+ if parent not in sys.modules:
132
+ self.load_module(parent)
133
+ else:
134
+ module.__package__ = fullname
135
+
136
+ sys.modules[fullname] = module
137
+ return module
138
+
139
+
140
+ def _normalize_package_name(name: str) -> str:
141
+ """Normalize package name for comparison (PEP 503)."""
142
+ return name.lower().replace('-', '_').replace('.', '_')
143
+
144
+
145
+ def _get_import_names_from_pixi(node_dir: Path) -> Set[str]:
146
+ """
147
+ Get import names by scanning the pixi environment's site-packages.
148
+
149
+ Finds all importable packages by looking for:
150
+ 1. Directories with __init__.py (packages)
151
+ 2. .py files (single-file modules)
152
+ 3. .so/.pyd files (extension modules)
153
+
154
+ Returns:
155
+ Set of import names that should be stubbed.
156
+ """
157
+ import_names = set()
158
+
159
+ # Find the pixi site-packages
160
+ pixi_lib = node_dir / ".pixi" / "envs" / "default" / "lib"
161
+
162
+ if not pixi_lib.exists():
163
+ return import_names
164
+
165
+ # Find the python version directory (e.g., python3.11)
166
+ python_dirs = list(pixi_lib.glob("python3.*"))
167
+ if not python_dirs:
168
+ return import_names
169
+
170
+ site_packages = python_dirs[0] / "site-packages"
171
+ if not site_packages.exists():
172
+ return import_names
173
+
174
+ # Scan for importable modules
175
+ for item in site_packages.iterdir():
176
+ name = item.name
177
+
178
+ # Skip private/internal items
179
+ if name.startswith('_') or name.startswith('.'):
180
+ continue
181
+
182
+ # Skip dist-info and egg-info directories
183
+ if name.endswith('.dist-info') or name.endswith('.egg-info'):
184
+ continue
185
+
186
+ # Skip common non-module items
187
+ if name in {'bin', 'share', 'include', 'etc'}:
188
+ continue
189
+
190
+ # Package directory (has __init__.py)
191
+ if item.is_dir():
192
+ if (item / "__init__.py").exists():
193
+ import_names.add(name)
194
+ continue
195
+
196
+ # Single-file module (.py)
197
+ if name.endswith('.py'):
198
+ import_names.add(name[:-3])
199
+ continue
200
+
201
+ # Extension module (.so on Linux, .pyd on Windows)
202
+ if '.cpython-' in name and (name.endswith('.so') or name.endswith('.pyd')):
203
+ # Extract module name: foo.cpython-311-x86_64-linux-gnu.so -> foo
204
+ module_name = name.split('.')[0]
205
+ import_names.add(module_name)
206
+ continue
207
+
208
+ return import_names
209
+
210
+
211
+ def _filter_to_missing(import_names: Set[str]) -> Set[str]:
212
+ """Filter to only imports not available in host Python."""
213
+ missing = set()
214
+
215
+ for name in import_names:
216
+ # Skip if already in sys.modules
217
+ if name in sys.modules:
218
+ continue
219
+
220
+ # Try to import
221
+ try:
222
+ __import__(name)
223
+ except ImportError:
224
+ missing.add(name)
225
+ except Exception:
226
+ # Other errors - don't stub, let real error surface
227
+ pass
228
+
229
+ return missing
230
+
231
+
232
+ # Track whether we've already set up stubs
233
+ _stub_finder: Optional[_StubFinder] = None
234
+
235
+
236
+ def setup_isolated_imports(init_file: str) -> List[str]:
237
+ """
238
+ Set up import stubs for packages in the pixi environment but not in host Python.
239
+
240
+ Call this BEFORE importing your nodes module.
241
+
242
+ Args:
243
+ init_file: The __file__ of the calling module (usually __file__ from __init__.py)
244
+
245
+ Returns:
246
+ List of import names that were stubbed.
247
+
248
+ Example:
249
+ from comfy_env import setup_isolated_imports
250
+ setup_isolated_imports(__file__)
251
+
252
+ from .nodes import NODE_CLASS_MAPPINGS # Now works!
253
+ """
254
+ global _stub_finder
255
+
256
+ node_dir = Path(init_file).resolve().parent
257
+
258
+ # Get all import names from pixi environment
259
+ pixi_imports = _get_import_names_from_pixi(node_dir)
260
+
261
+ if not pixi_imports:
262
+ print("[comfy-env] No pixi environment found, skipping import stubbing")
263
+ return []
264
+
265
+ # Filter to only those missing in host
266
+ missing = _filter_to_missing(pixi_imports)
267
+
268
+ if not missing:
269
+ print("[comfy-env] All pixi packages available in host, no stubbing needed")
270
+ return []
271
+
272
+ # Remove old finder if exists
273
+ if _stub_finder is not None:
274
+ try:
275
+ sys.meta_path.remove(_stub_finder)
276
+ except ValueError:
277
+ pass
278
+
279
+ # Register new finder
280
+ _stub_finder = _StubFinder(missing)
281
+ sys.meta_path.insert(0, _stub_finder)
282
+
283
+ stubbed = sorted(missing)
284
+ if len(stubbed) <= 10:
285
+ print(f"[comfy-env] Stubbed {len(stubbed)} imports: {', '.join(stubbed)}")
286
+ else:
287
+ print(f"[comfy-env] Stubbed {len(stubbed)} imports: {', '.join(stubbed[:10])}... and {len(stubbed)-10} more")
288
+
289
+ return stubbed
290
+
291
+
292
+ def cleanup_stubs():
293
+ """Remove the stub import hooks."""
294
+ global _stub_finder
295
+
296
+ if _stub_finder is not None:
297
+ try:
298
+ sys.meta_path.remove(_stub_finder)
299
+ except ValueError:
300
+ pass
301
+
302
+ # Remove stubbed modules from sys.modules
303
+ to_remove = [
304
+ name for name in sys.modules
305
+ if isinstance(sys.modules[name], _StubModule)
306
+ ]
307
+ for name in to_remove:
308
+ del sys.modules[name]
309
+
310
+ _stub_finder = None
@@ -31,9 +31,10 @@ Case 1: Just need CUDA packages (nvdiffrast, pytorch3d, etc.)
31
31
  - Add packages to [cuda] section
32
32
  - Call install() in your __init__.py
33
33
 
34
- Case 2: Need process isolation (conflicting dependencies)
35
- - Define an isolated environment section like [myenv]
36
- - Use @isolated(env="myenv") decorator on your node class
34
+ Case 2: Need process isolation (conflicting dependencies, conda packages)
35
+ - Define an isolated environment with `isolated = true`
36
+ - Use enable_isolation(NODE_CLASS_MAPPINGS) in your __init__.py
37
+ - See PROCESS ISOLATION section below
37
38
 
38
39
  Case 3: Need system packages (apt)
39
40
  - Add to [system] linux = ["package1", "package2"]
@@ -54,22 +55,42 @@ PROCESS ISOLATION
54
55
  -----------------
55
56
  For nodes that need isolated dependencies:
56
57
 
58
+ RECOMMENDED: Pack-wide isolation (all nodes in same isolated env)
59
+
60
+ from comfy_env import setup_isolated_imports, enable_isolation
61
+
62
+ # Setup import stubs BEFORE importing nodes
63
+ setup_isolated_imports(__file__)
64
+
65
+ from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
66
+
67
+ # Enable isolation for all nodes
68
+ enable_isolation(NODE_CLASS_MAPPINGS)
69
+
70
+ Requires `isolated = true` in comfy-env.toml:
71
+
72
+ [mypack]
73
+ python = "3.11"
74
+ isolated = true
75
+
76
+ [mypack.packages]
77
+ requirements = ["trimesh", "scipy"]
78
+
79
+ ALTERNATIVE: Per-node isolation (for multiple isolated envs)
80
+
57
81
  from comfy_env import isolated
58
82
 
59
83
  @isolated(env="myenv")
60
84
  class MyNode:
61
85
  FUNCTION = "process"
62
- RETURN_TYPES = ("IMAGE",)
63
-
64
86
  def process(self, image):
65
- # This runs in isolated subprocess
66
87
  import conflicting_lib
67
88
  return (result,)
68
89
 
69
- The decorator:
70
- - Runs your node's FUNCTION method in a separate Python process
71
- - Transfers tensors efficiently via PyTorch IPC (zero-copy for CUDA)
72
- - Uses the venv defined in your comfy-env.toml [myenv] section
90
+ How it works:
91
+ - Runs FUNCTION methods in a separate Python process
92
+ - Tensors/numpy arrays passed by value (efficient)
93
+ - Complex objects (meshes, etc.) passed by reference
73
94
 
74
95
 
75
96
  TROUBLESHOOTING
@@ -100,82 +100,71 @@ requirements = []
100
100
  #
101
101
  # #############################################################################
102
102
  #
103
- # For nodes that need completely isolated dependencies (different PyTorch
104
- # version, conflicting native libraries, etc.), define isolated environments.
103
+ # For nodes that need completely isolated dependencies (different Python
104
+ # version, conda packages, conflicting native libraries), define an isolated
105
+ # environment with `isolated = true`.
105
106
  #
106
- # How it works:
107
- # 1. Define an environment below with its own Python/CUDA/packages
108
- # 2. Use the @isolated decorator on your node class
109
- # 3. The node runs in a separate subprocess with its own venv
107
+ # RECOMMENDED: Pack-wide isolation (all nodes in one environment)
108
+ # ----------------------------------------------------------------
109
+ # This is the simplest approach - all your nodes run in the same isolated env.
110
110
  #
111
- # Example in __init__.py:
111
+ # Step 1: Define environment in comfy-env.toml (this file)
112
+ # Step 2: In __init__.py:
112
113
  #
113
- # from comfy_env import isolated
114
+ # from comfy_env import setup_isolated_imports, enable_isolation
115
+ #
116
+ # # Setup import stubs BEFORE importing nodes
117
+ # setup_isolated_imports(__file__)
114
118
  #
115
- # @isolated(env="myenv")
116
- # class MyIsolatedNode:
117
- # FUNCTION = "process"
118
- # RETURN_TYPES = ("IMAGE",)
119
+ # from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
119
120
  #
120
- # def process(self, image):
121
- # # This runs in isolated subprocess with its own dependencies
122
- # import conflicting_package
123
- # return (result,)
121
+ # # Enable isolation for all nodes
122
+ # enable_isolation(NODE_CLASS_MAPPINGS)
124
123
  #
125
124
  # =============================================================================
126
125
 
127
126
 
128
127
  # -----------------------------------------------------------------------------
129
- # Example: Simple isolation (same Python, different process)
128
+ # Example: Full pack isolation with conda packages (RECOMMENDED)
130
129
  # -----------------------------------------------------------------------------
131
- # Use when you just need process isolation but no venv (fastest option).
132
- # In your code: @isolated(env="simple", same_venv=True)
130
+ # Uses pixi to create an isolated environment with conda + pip packages.
133
131
 
134
- # [simple]
132
+ # [mypack]
135
133
  # python = "3.11"
136
- # cuda_version = "auto"
137
- # pytorch_version = "auto"
138
-
139
-
140
- # -----------------------------------------------------------------------------
141
- # Example: Full venv isolation
142
- # -----------------------------------------------------------------------------
143
- # Use when you need completely different dependencies.
144
- # Creates a separate virtual environment with its own packages.
145
-
146
- # [myenv]
147
- # python = "3.10"
148
- # cuda_version = "12.8"
149
- # pytorch_version = "2.8.0"
134
+ # isolated = true # Required for enable_isolation()
150
135
  #
151
- # [myenv.cuda]
152
- # nvdiffrast = "0.4.0"
153
- # pytorch3d = "0.7.9"
136
+ # [mypack.conda]
137
+ # channels = ["conda-forge"]
138
+ # packages = ["cgal", "openmesh"]
154
139
  #
155
- # [myenv.packages]
156
- # requirements = ["trimesh", "scipy"]
140
+ # [mypack.packages]
141
+ # requirements = ["trimesh[easy]>=4.0", "numpy", "scipy"]
157
142
 
158
143
 
159
144
  # -----------------------------------------------------------------------------
160
- # Example: Multiple isolated environments
145
+ # Example: Multiple isolated environments (per-node control)
161
146
  # -----------------------------------------------------------------------------
162
- # Some nodes need multiple different environments for different operations.
147
+ # Use @isolated(env="envname") decorator when different nodes need different envs.
148
+ #
149
+ # from comfy_env import isolated
150
+ #
151
+ # @isolated(env="env-preprocessing")
152
+ # class PreprocessNode: ...
153
+ #
154
+ # @isolated(env="env-inference")
155
+ # class InferenceNode: ...
163
156
 
164
157
  # [env-preprocessing]
165
158
  # python = "3.11"
166
- # cuda_version = "12.8"
167
- # pytorch_version = "2.8.0"
168
159
  #
169
160
  # [env-preprocessing.packages]
170
161
  # requirements = ["opencv-python-headless", "pillow"]
171
162
 
172
163
  # [env-inference]
173
164
  # python = "3.10"
174
- # cuda_version = "12.4"
175
- # pytorch_version = "2.5.1"
176
165
  #
177
166
  # [env-inference.cuda]
178
- # torch-scatter = "2.1.2" # Use exact name from `comfy-env list-packages`
167
+ # torch-scatter = "2.1.2"
179
168
 
180
169
 
181
170
  # -----------------------------------------------------------------------------
@@ -185,7 +174,7 @@ requirements = []
185
174
 
186
175
  # [crossplatform]
187
176
  # python = "3.11"
188
- # cuda_version = "auto"
177
+ # isolated = true
189
178
  #
190
179
  # [crossplatform.packages]
191
180
  # requirements = ["numpy", "pillow"]
@@ -195,17 +184,3 @@ requirements = []
195
184
  #
196
185
  # [crossplatform.packages.linux]
197
186
  # requirements = ["python-xlib"]
198
-
199
-
200
- # -----------------------------------------------------------------------------
201
- # Example: Conda packages (uses pixi backend)
202
- # -----------------------------------------------------------------------------
203
- # For packages that are easier to install via conda (e.g., CUDA toolkit).
204
-
205
- # [blender-env]
206
- # python = "3.11"
207
- # cuda_version = "12.8"
208
- #
209
- # [blender-env.conda]
210
- # channels = ["conda-forge", "nvidia"]
211
- # packages = ["cuda-toolkit=12.8", "bpy"]