Coverage for openhcs/utils/enum_factory.py: 27.7%
96 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-04 02:09 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-04 02:09 +0000
1"""
2Dynamic enum creation utilities for OpenHCS.
4Provides functions for creating enums dynamically from introspection,
5particularly for visualization colormaps and other runtime-discovered options.
7Caching:
8- Colormap enums are cached to avoid expensive napari/matplotlib imports
9- Cache invalidated on OpenHCS version change or after 30 days
10- Provides ~20x speedup on subsequent runs
11"""
12from enum import Enum
13from typing import List, Callable, Optional, Dict, Any
14import logging
15from openhcs.utils.environment import is_headless_mode
17logger = logging.getLogger(__name__)
20# Lazy import cache manager to avoid circular dependencies
21_cache_manager = None
24def _get_colormap_cache_manager():
25 """Lazy import of cache manager for colormap enums."""
26 global _cache_manager
27 if _cache_manager is None:
28 try:
29 from openhcs.core.registry_cache import RegistryCacheManager, CacheConfig
31 def get_version():
32 try:
33 import openhcs
34 return openhcs.__version__
35 except:
36 return "unknown"
38 # Serializer for enum members (just store the dict)
39 def serialize_enum_members(members: Dict[str, str]) -> Dict[str, Any]:
40 return {'members': members}
42 # Deserializer for enum members
43 def deserialize_enum_members(data: Dict[str, Any]) -> Dict[str, str]:
44 return data.get('members', {})
46 _cache_manager = RegistryCacheManager(
47 cache_name="colormap_enum",
48 version_getter=get_version,
49 serializer=serialize_enum_members,
50 deserializer=deserialize_enum_members,
51 config=CacheConfig(
52 max_age_days=30, # Longer cache for stable enums
53 check_mtimes=False # No file tracking needed for external libs
54 )
55 )
56 except Exception as e:
57 logger.debug(f"Failed to initialize colormap cache manager: {e}")
58 _cache_manager = False # Mark as failed to avoid retrying
60 return _cache_manager if _cache_manager is not False else None
63def get_available_colormaps() -> List[str]:
64 """
65 Get available colormaps using introspection - napari first, then matplotlib.
67 In headless/CI contexts, avoid importing viz libs; return minimal stable set.
69 Returns:
70 List of available colormap names
71 """
72 if is_headless_mode():
73 return ['gray', 'viridis']
75 try:
76 from napari.utils.colormaps import AVAILABLE_COLORMAPS
77 return list(AVAILABLE_COLORMAPS.keys())
78 except ImportError:
79 pass
81 try:
82 import matplotlib.pyplot as plt
83 return list(plt.colormaps())
84 except ImportError:
85 pass
87 raise ImportError("Neither napari nor matplotlib colormaps are available. Install napari or matplotlib.")
90def create_colormap_enum(lazy: bool = False, enable_cache: bool = True) -> Enum:
91 """
92 Create a dynamic enum for available colormaps using pure introspection.
94 Caching is enabled by default to avoid expensive napari/matplotlib imports
95 on subsequent runs (~20x speedup).
97 Args:
98 lazy: If True, use minimal colormap set without importing napari/matplotlib.
99 This avoids blocking imports (napari → dask → GPU libs).
100 enable_cache: If True, use persistent cache for enum members (default: True)
102 Returns:
103 Enum class with colormap names as members
105 Raises:
106 ValueError: If no colormaps are available or no valid identifiers could be created
107 """
108 # Try to load from cache first (if not lazy mode)
109 cache_manager = _get_colormap_cache_manager() if enable_cache and not lazy else None
111 if cache_manager: 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true
112 try:
113 cached_data = cache_manager.load_cache()
114 if cached_data is not None:
115 # Cache hit - reconstruct enum from cached members
116 members = cached_data
117 logger.debug(f"✅ Loaded {len(members)} colormap enum members from cache")
119 NapariColormap = Enum('NapariColormap', members)
120 NapariColormap.__module__ = 'openhcs.core.config'
121 NapariColormap.__qualname__ = 'NapariColormap'
122 return NapariColormap
123 except Exception as e:
124 logger.debug(f"Cache load failed for colormap enum: {e}")
126 # Cache miss or disabled - discover colormaps
127 if lazy: 127 ↛ 131line 127 didn't jump to line 131 because the condition on line 127 was always true
128 # Use minimal set without importing visualization libraries
129 available_cmaps = ['gray', 'viridis', 'magma', 'inferno', 'plasma', 'cividis']
130 else:
131 available_cmaps = get_available_colormaps()
133 if not available_cmaps: 133 ↛ 134line 133 didn't jump to line 134 because the condition on line 133 was never true
134 raise ValueError("No colormaps available for enum creation")
136 members = {}
137 for cmap_name in available_cmaps:
138 enum_name = cmap_name.replace(' ', '_').replace('-', '_').replace('.', '_').upper()
139 if enum_name and enum_name[0].isdigit(): 139 ↛ 140line 139 didn't jump to line 140 because the condition on line 139 was never true
140 enum_name = f"CMAP_{enum_name}"
141 if enum_name and enum_name.replace('_', '').replace('CMAP', '').isalnum(): 141 ↛ 137line 141 didn't jump to line 137 because the condition on line 141 was always true
142 members[enum_name] = cmap_name
144 if not members: 144 ↛ 145line 144 didn't jump to line 145 because the condition on line 144 was never true
145 raise ValueError("No valid colormap identifiers could be created")
147 # Save to cache if enabled
148 if cache_manager: 148 ↛ 149line 148 didn't jump to line 149 because the condition on line 148 was never true
149 try:
150 cache_manager.save_cache(members)
151 logger.debug(f"💾 Saved {len(members)} colormap enum members to cache")
152 except Exception as e:
153 logger.debug(f"Failed to save colormap enum cache: {e}")
155 NapariColormap = Enum('NapariColormap', members)
157 # Set proper module and qualname for pickling support
158 NapariColormap.__module__ = 'openhcs.core.config'
159 NapariColormap.__qualname__ = 'NapariColormap'
161 return NapariColormap
164def create_enum_from_source(
165 enum_name: str,
166 source_func: Callable[[], List[str]],
167 name_transform: Optional[Callable[[str], str]] = None
168) -> Enum:
169 """
170 Generic factory for creating enums from introspection source functions.
172 Args:
173 enum_name: Name for the created enum class
174 source_func: Function that returns list of string values for enum members
175 name_transform: Optional function to transform value strings to enum member names
177 Returns:
178 Dynamically created Enum class
180 Example:
181 >>> def get_luts():
182 ... return ['Grays', 'Fire', 'Ice']
183 >>> FijiLUT = create_enum_from_source('FijiLUT', get_luts)
184 """
185 values = source_func()
186 if not values:
187 raise ValueError(f"No values available for {enum_name} creation")
189 members = {}
190 for value in values:
191 if name_transform:
192 member_name = name_transform(value)
193 else:
194 member_name = value.replace(' ', '_').replace('-', '_').replace('.', '_').upper()
195 if member_name and member_name[0].isdigit():
196 member_name = f"VAL_{member_name}"
198 if member_name and member_name.replace('_', '').replace('VAL', '').isalnum():
199 members[member_name] = value
201 if not members:
202 raise ValueError(f"No valid identifiers could be created for {enum_name}")
204 EnumClass = Enum(enum_name, members)
206 # Set proper module and qualname for pickling support
207 EnumClass.__module__ = 'openhcs.core.config'
208 EnumClass.__qualname__ = enum_name
210 return EnumClass