diff --git a/src/Mod/CAM/CAMTests/TestPathToolAssetManager.py b/src/Mod/CAM/CAMTests/TestPathToolAssetManager.py index ea5fd91ec2..ac23e43556 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolAssetManager.py +++ b/src/Mod/CAM/CAMTests/TestPathToolAssetManager.py @@ -313,8 +313,9 @@ class TestPathToolAssetManager(unittest.TestCase): non_existent_uri = AssetUri("type://id/1") with self.assertRaises(FileNotFoundError) as cm: manager.get_raw(non_existent_uri, store="non_existent_store") - self.assertIn("Asset 'type://id/1' not found in stores '['non_existent_store']'.", - str(cm.exception)) + self.assertIn( + "Asset 'type://id/1' not found in stores '['non_existent_store']'.", str(cm.exception) + ) def test_is_empty(self): # Setup AssetManager with a real MemoryStore diff --git a/src/Mod/CAM/Path/Tool/assets/manager.py b/src/Mod/CAM/Path/Tool/assets/manager.py index 537ec2a7c6..a2cd1f6651 100644 --- a/src/Mod/CAM/Path/Tool/assets/manager.py +++ b/src/Mod/CAM/Path/Tool/assets/manager.py @@ -57,9 +57,7 @@ class _AssetConstructionData: raw_data: bytes asset_class: Type[Asset] # Stores AssetConstructionData for dependencies, keyed by their AssetUri - dependencies_data: Optional[ - Dict[AssetUri, Optional["_AssetConstructionData"]] - ] = None + dependencies_data: Optional[Dict[AssetUri, Optional["_AssetConstructionData"]]] = None class AssetManager: @@ -69,15 +67,11 @@ class AssetManager: self._asset_classes: Dict[str, Type[Asset]] = {} self.asset_cache = AssetCache(max_size_bytes=cache_max_size_bytes) self._cacheable_stores: Set[str] = set() - logger.debug( - f"AssetManager initialized (Thread: {threading.current_thread().name})" - ) + logger.debug(f"AssetManager initialized (Thread: {threading.current_thread().name})") def register_store(self, store: AssetStore, cacheable: bool = False): """Registers an AssetStore with the manager.""" - logger.debug( - f"Registering store: {store.name}, cacheable: {cacheable}" - ) + logger.debug(f"Registering store: {store.name}, cacheable: {cacheable}") self.stores[store.name] = store if cacheable: self._cacheable_stores.add(store.name) @@ -88,31 +82,21 @@ class AssetManager: return serializer raise ValueError(f"No serializer found for class {asset_class}") - def register_asset( - self, asset_class: Type[Asset], serializer: Type[AssetSerializer] - ): + def register_asset(self, asset_class: Type[Asset], serializer: Type[AssetSerializer]): """Registers an Asset class with the manager.""" if not issubclass(asset_class, Asset): - raise TypeError( - f"Item '{asset_class.__name__}' must be a subclass of Asset." - ) + raise TypeError(f"Item '{asset_class.__name__}' must be a subclass of Asset.") if not issubclass(serializer, AssetSerializer): - raise TypeError( - f"Item '{serializer.__name__}' must be a subclass of AssetSerializer." - ) + raise TypeError(f"Item '{serializer.__name__}' must be a subclass of AssetSerializer.") self._serializers.append((serializer, asset_class)) asset_type_name = getattr(asset_class, "asset_type", None) - if ( - not isinstance(asset_type_name, str) or not asset_type_name - ): # Ensure not empty + if not isinstance(asset_type_name, str) or not asset_type_name: # Ensure not empty raise TypeError( f"Asset class '{asset_class.__name__}' must have a non-empty string 'asset_type' attribute." ) - logger.debug( - f"Registering asset type: '{asset_type_name}' -> {asset_class.__name__}" - ) + logger.debug(f"Registering asset type: '{asset_type_name}' -> {asset_class.__name__}") self._asset_classes[asset_type_name] = asset_class async def _fetch_asset_construction_data_recursive_async( @@ -136,9 +120,7 @@ class AssetManager: asset_class = self._asset_classes.get(uri.asset_type) if not asset_class: - raise ValueError( - f"No asset class registered for asset type: {asset_class}" - ) + raise ValueError(f"No asset class registered for asset type: {asset_class}") # Fetch the requested asset, trying each store in order raw_data = None @@ -146,9 +128,7 @@ class AssetManager: for current_store_name in store_names: store = self.stores.get(current_store_name) if not store: - logger.warning( - f"Store '{current_store_name}' not registered. Skipping." - ) + logger.warning(f"Store '{current_store_name}' not registered. Skipping.") continue try: @@ -178,15 +158,11 @@ class AssetManager: # Extract the list of dependencies (non-recursive) serializer = self.get_serializer_for_class(asset_class) - dependency_uris = asset_class.extract_dependencies( - raw_data, serializer - ) + dependency_uris = asset_class.extract_dependencies(raw_data, serializer) # Initialize deps_construction_data_map. Any dependencies mapped to None # indicate that dependencies were intentionally not fetched. - deps_construction_data: Dict[ - AssetUri, Optional[_AssetConstructionData] - ] = {} + deps_construction_data: Dict[AssetUri, Optional[_AssetConstructionData]] = {} for dep_uri in dependency_uris: visited_uris.add(uri) @@ -226,15 +202,10 @@ class AssetManager: deps_signature_tuple: Tuple = ("shallow_children",) else: deps_signature_tuple = tuple( - sorted( - str(uri) - for uri in construction_data.dependencies_data.keys() - ) + sorted(str(uri) for uri in construction_data.dependencies_data.keys()) ) - raw_data_hash = int( - hashlib.sha256(construction_data.raw_data).hexdigest(), 16 - ) + raw_data_hash = int(hashlib.sha256(construction_data.raw_data).hexdigest(), 16) return CacheKey( store_name=store_name_for_cache, @@ -283,9 +254,7 @@ class AssetManager: # this would need more complex store_name propagation. # For now, use the parent's store_name_for_cache. try: - dep = self._build_asset_tree_from_data_sync( - dep_data_node, store_name_for_cache - ) + dep = self._build_asset_tree_from_data_sync(dep_data_node, store_name_for_cache) except Exception as e: logger.error( f"Error building dependency '{dep_uri}' for asset '{construction_data.uri}': {e}", @@ -315,8 +284,7 @@ class AssetManager: direct_deps_uris_strs: Set[str] = set() if construction_data.dependencies_data is not None: direct_deps_uris_strs = { - str(uri) - for uri in construction_data.dependencies_data.keys() + str(uri) for uri in construction_data.dependencies_data.keys() } raw_data_size = len(construction_data.raw_data) self.asset_cache.put( @@ -347,8 +315,7 @@ class AssetManager: ) if ( QtGui.QApplication.instance() - and QtCore.QThread.currentThread() - is not QtGui.QApplication.instance().thread() + and QtCore.QThread.currentThread() is not QtGui.QApplication.instance().thread() ): logger.warning( "AssetManager.get() called from a non-main thread! UI in from_bytes may fail!" @@ -378,9 +345,7 @@ class AssetManager: if all_construction_data is None: # This means the top-level asset itself was not found by _fetch_... - raise FileNotFoundError( - f"Asset '{asset_uri_obj}' not found in stores '{stores_list}'." - ) + raise FileNotFoundError(f"Asset '{asset_uri_obj}' not found in stores '{stores_list}'.") # Step 2: Synchronously build the asset tree (and call from_bytes) # This happens in the current thread (which is assumed to be the main UI thread) @@ -403,9 +368,7 @@ class AssetManager: final_asset = self._build_asset_tree_from_data_sync( all_construction_data, store_name_for_cache=store_name_for_cache ) - logger.debug( - f"Get: Synchronous asset tree build for '{asset_uri_obj}' completed." - ) + logger.debug(f"Get: Synchronous asset tree build for '{asset_uri_obj}' completed.") return final_asset def get_or_none( @@ -443,10 +406,8 @@ class AssetManager: asset_uri_obj = AssetUri(uri) if isinstance(uri, str) else uri - all_construction_data = ( - await self._fetch_asset_construction_data_recursive_async( - asset_uri_obj, stores_list, set(), depth - ) + all_construction_data = await self._fetch_asset_construction_data_recursive_async( + asset_uri_obj, stores_list, set(), depth ) if all_construction_data is None: @@ -484,9 +445,7 @@ class AssetManager: for current_store_name in stores_list: store = self.stores.get(current_store_name) if not store: - logger.warning( - f"Store '{current_store_name}' not registered. Skipping." - ) + logger.warning(f"Store '{current_store_name}' not registered. Skipping.") continue try: raw_data = await store.get(asset_uri_obj) @@ -499,9 +458,7 @@ class AssetManager: f"GetRawAsync: Asset {asset_uri_obj} not found in store {current_store_name}" ) continue - raise FileNotFoundError( - f"Asset '{asset_uri_obj}' not found in stores '{stores_list}'." - ) + raise FileNotFoundError(f"Asset '{asset_uri_obj}' not found in stores '{stores_list}'.") try: return asyncio.run(_fetch_raw_async(stores_list)) @@ -527,9 +484,7 @@ class AssetManager: for current_store_name in stores_list: store = self.stores.get(current_store_name) if not store: - logger.warning( - f"Store '{current_store_name}' not registered. Skipping." - ) + logger.warning(f"Store '{current_store_name}' not registered. Skipping.") continue try: raw_data = await store.get(asset_uri_obj) @@ -543,9 +498,7 @@ class AssetManager: ) continue - raise FileNotFoundError( - f"Asset '{asset_uri_obj}' not found in stores '{stores_list}'." - ) + raise FileNotFoundError(f"Asset '{asset_uri_obj}' not found in stores '{stores_list}'.") def get_bulk( self, @@ -575,13 +528,9 @@ class AssetManager: try: logger.debug("GetBulk: Starting bulk data fetching") - all_construction_data_list = asyncio.run( - _fetch_all_construction_data_bulk_async() - ) + all_construction_data_list = asyncio.run(_fetch_all_construction_data_bulk_async()) logger.debug("GetBulk: bulk data fetching completed") - except ( - Exception - ) as e: # Should ideally not happen if gather returns exceptions + except Exception as e: # Should ideally not happen if gather returns exceptions logger.error( f"GetBulk: Unexpected error during asyncio.run for bulk data: {e}", exc_info=False, @@ -601,20 +550,14 @@ class AssetManager: elif isinstance(data_or_exc, _AssetConstructionData): # Build asset instance synchronously. Exceptions during build should propagate. # Use the first store from the list for caching purposes in build_asset_tree - store_name_for_cache = ( - stores_list[0] if stores_list else "local" - ) + store_name_for_cache = stores_list[0] if stores_list else "local" assets.append( self._build_asset_tree_from_data_sync( data_or_exc, store_name_for_cache=store_name_for_cache ) ) - elif ( - data_or_exc is None - ): # From _fetch_... returning None for not found - logger.debug( - f"GetBulk: Asset '{original_uri_input}' not found" - ) + elif data_or_exc is None: # From _fetch_... returning None for not found + logger.debug(f"GetBulk: Asset '{original_uri_input}' not found") assets.append(None) else: # Should not happen logger.error( @@ -646,26 +589,19 @@ class AssetManager: ) for u in uris ] - all_construction_data_list = await asyncio.gather( - *tasks, return_exceptions=True - ) + all_construction_data_list = await asyncio.gather(*tasks, return_exceptions=True) assets = [] for i, data_or_exc in enumerate(all_construction_data_list): if isinstance(data_or_exc, _AssetConstructionData): # Use the first store from the list for caching purposes in build_asset_tree - store_name_for_cache = ( - stores_list[0] if stores_list else "local" - ) + store_name_for_cache = stores_list[0] if stores_list else "local" assets.append( self._build_asset_tree_from_data_sync( data_or_exc, store_name_for_cache=store_name_for_cache ) ) - elif ( - isinstance(data_or_exc, FileNotFoundError) - or data_or_exc is None - ): + elif isinstance(data_or_exc, FileNotFoundError) or data_or_exc is None: assets.append(None) elif isinstance(data_or_exc, Exception): assets.append(data_or_exc) # Caller must check @@ -688,9 +624,7 @@ class AssetManager: for current_store_name in stores_list: store = self.stores.get(current_store_name) if not store: - logger.warning( - f"Store '{current_store_name}' not registered. Skipping." - ) + logger.warning(f"Store '{current_store_name}' not registered. Skipping.") continue try: exists = await store.exists(asset_uri_obj) @@ -732,17 +666,13 @@ class AssetManager: ) -> List[Asset]: """Fetches asset instances based on type, limit, and offset (synchronous), to a specified depth.""" stores_list = [store] if isinstance(store, str) else store - logger.debug( - f"Fetch(type='{asset_type}', stores='{stores_list}', depth='{depth}')" - ) + logger.debug(f"Fetch(type='{asset_type}', stores='{stores_list}', depth='{depth}')") # Note: list_assets currently only supports a single store. # If fetching from multiple stores is needed for listing, this needs # to be updated. For now, list from the first store. list_store = stores_list[0] if stores_list else "local" asset_uris = self.list_assets(asset_type, limit, offset, list_store) - results = self.get_bulk( - asset_uris, stores_list, depth - ) # Pass stores_list to get_bulk + results = self.get_bulk(asset_uris, stores_list, depth) # Pass stores_list to get_bulk # Filter out non-Asset objects (e.g., None for not found, or exceptions if collected) return [asset for asset in results if isinstance(asset, Asset)] @@ -756,16 +686,12 @@ class AssetManager: ) -> List[Asset]: """Fetches asset instances based on type, limit, and offset (asynchronous), to a specified depth.""" stores_list = [store] if isinstance(store, str) else store - logger.debug( - f"FetchAsync(type='{asset_type}', stores='{stores_list}', depth='{depth}')" - ) + logger.debug(f"FetchAsync(type='{asset_type}', stores='{stores_list}', depth='{depth}')") # Note: list_assets_async currently only supports a single store. # If fetching from multiple stores is needed for listing, this needs # to be updated. For now, list from the first store. list_store = stores_list[0] if stores_list else "local" - asset_uris = await self.list_assets_async( - asset_type, limit, offset, list_store - ) + asset_uris = await self.list_assets_async(asset_type, limit, offset, list_store) results = await self.get_bulk_async( asset_uris, stores_list, depth ) # Pass stores_list to get_bulk_async @@ -779,16 +705,12 @@ class AssetManager: store: Union[str, Sequence[str]] = "local", ) -> List[AssetUri]: stores_list = [store] if isinstance(store, str) else store - logger.debug( - f"ListAssets(type='{asset_type}', stores='{stores_list}')" - ) + logger.debug(f"ListAssets(type='{asset_type}', stores='{stores_list}')") # Note: list_assets_async currently only supports a single store. # If listing from multiple stores is needed, this needs to be updated. # For now, list from the first store. list_store = stores_list[0] if stores_list else "local" - return asyncio.run( - self.list_assets_async(asset_type, limit, offset, list_store) - ) + return asyncio.run(self.list_assets_async(asset_type, limit, offset, list_store)) async def list_assets_async( self, @@ -798,9 +720,7 @@ class AssetManager: store: Union[str, Sequence[str]] = "local", ) -> List[AssetUri]: stores_list = [store] if isinstance(store, str) else store - logger.debug( - f"ListAssetsAsync executing for type='{asset_type}', stores='{stores_list}'" - ) + logger.debug(f"ListAssetsAsync executing for type='{asset_type}', stores='{stores_list}'") # Note: list_assets_async currently only supports a single store. # If listing from multiple stores is needed, this needs to be updated. # For now, list from the first store. @@ -820,9 +740,7 @@ class AssetManager: store: Union[str, Sequence[str]] = "local", ) -> int: stores_list = [store] if isinstance(store, str) else store - logger.debug( - f"CountAssets(type='{asset_type}', stores='{stores_list}')" - ) + logger.debug(f"CountAssets(type='{asset_type}', stores='{stores_list}')") # Note: count_assets_async currently only supports a single store. # If counting across multiple stores is needed, this needs to be updated. # For now, count from the first store. @@ -835,9 +753,7 @@ class AssetManager: store: Union[str, Sequence[str]] = "local", ) -> int: stores_list = [store] if isinstance(store, str) else store - logger.debug( - f"CountAssetsAsync executing for type='{asset_type}', stores='{stores_list}'" - ) + logger.debug(f"CountAssetsAsync executing for type='{asset_type}', stores='{stores_list}'") # Note: count_assets_async currently only supports a single store. # If counting across multiple stores is needed, this needs to be updated. # For now, count from the first store. @@ -863,20 +779,14 @@ class AssetManager: Adds an asset to the store, either creating a new one or updating an existing one. Uses obj.get_url() to determine if the asset exists. """ - logger.debug( - f"AddAsync: Adding {type(obj).__name__} to store '{store}'" - ) + logger.debug(f"AddAsync: Adding {type(obj).__name__} to store '{store}'") uri = obj.get_uri() if not self._is_registered_type(obj): - logger.warning( - f"Asset has unregistered type '{uri.asset_type}' ({type(obj).__name__})" - ) + logger.warning(f"Asset has unregistered type '{uri.asset_type}' ({type(obj).__name__})") serializer = self.get_serializer_for_class(obj.__class__) data = obj.to_bytes(serializer) - return await self.add_raw_async( - uri.asset_type, uri.asset_id, data, store - ) + return await self.add_raw_async(uri.asset_type, uri.asset_id, data, store) def add(self, obj: Asset, store: str = "local") -> AssetUri: """Synchronous wrapper for adding an asset to the store.""" @@ -891,13 +801,9 @@ class AssetManager: """ Adds raw asset data to the store, either creating a new asset or updating an existing one. """ - logger.debug( - f"AddRawAsync: type='{asset_type}', id='{asset_id}', store='{store}'" - ) + logger.debug(f"AddRawAsync: type='{asset_type}', id='{asset_id}', store='{store}'") if not asset_type or not asset_id: - raise ValueError( - "asset_type and asset_id must be provided for add_raw." - ) + raise ValueError("asset_type and asset_id must be provided for add_raw.") if not isinstance(data, bytes): raise TypeError("Data for add_raw must be bytes.") selected_store = self.stores.get(store) @@ -914,9 +820,7 @@ class AssetManager: uri = await selected_store.create(asset_type, asset_id, data) if store in self._cacheable_stores: - self.asset_cache.invalidate_for_uri( - str(uri) - ) # Invalidate after add/update + self.asset_cache.invalidate_for_uri(str(uri)) # Invalidate after add/update return uri def add_raw( @@ -927,9 +831,7 @@ class AssetManager: f"AddRaw: type='{asset_type}', id='{asset_id}', store='{store}' from T:{threading.current_thread().name}" ) try: - return asyncio.run( - self.add_raw_async(asset_type, asset_id, data, store) - ) + return asyncio.run(self.add_raw_async(asset_type, asset_id, data, store)) except Exception as e: logger.error( f"AddRaw: Error for type='{asset_type}', id='{asset_id}': {e}", @@ -948,9 +850,7 @@ class AssetManager: Convenience wrapper around add_raw(). If asset_id is None, the path.stem is used as the id. """ - return self.add_raw( - asset_type, asset_id or path.stem, path.read_bytes(), store=store - ) + return self.add_raw(asset_type, asset_id or path.stem, path.read_bytes(), store=store) def delete(self, uri: Union[AssetUri, str], store: str = "local") -> None: logger.debug(f"Delete URI '{uri}' from store '{store}'") @@ -964,9 +864,7 @@ class AssetManager: asyncio.run(_do_delete_async()) - async def delete_async( - self, uri: Union[AssetUri, str], store: str = "local" - ) -> None: + async def delete_async(self, uri: Union[AssetUri, str], store: str = "local") -> None: logger.debug(f"DeleteAsync URI '{uri}' from store '{store}'") asset_uri_obj = AssetUri(uri) if isinstance(uri, str) else uri selected_store = self.stores[store] @@ -974,9 +872,7 @@ class AssetManager: if store in self._cacheable_stores: self.asset_cache.invalidate_for_uri(str(asset_uri_obj)) - async def is_empty_async( - self, asset_type: Optional[str] = None, store: str = "local" - ) -> bool: + async def is_empty_async(self, asset_type: Optional[str] = None, store: str = "local") -> bool: """Checks if the asset store has any assets of a given type (asynchronous).""" logger.debug(f"IsEmptyAsync: type='{asset_type}', store='{store}'") logger.debug( @@ -987,9 +883,7 @@ class AssetManager: raise ValueError(f"No store registered for name: {store}") return await selected_store.is_empty(asset_type) - def is_empty( - self, asset_type: Optional[str] = None, store: str = "local" - ) -> bool: + def is_empty(self, asset_type: Optional[str] = None, store: str = "local") -> bool: """Checks if the asset store has any assets of a given type (synchronous wrapper).""" logger.debug( f"IsEmpty: type='{asset_type}', store='{store}' from T:{threading.current_thread().name}" diff --git a/src/Mod/CAM/Path/Tool/camassets.py b/src/Mod/CAM/Path/Tool/camassets.py index 9c4a4f64eb..e244170b36 100644 --- a/src/Mod/CAM/Path/Tool/camassets.py +++ b/src/Mod/CAM/Path/Tool/camassets.py @@ -122,12 +122,14 @@ builtin_asset_store = FileStore( mapping=asset_mapping, ) + class CamAssetManager(AssetManager): """ Custom CAM Asset Manager that extends the base AssetManager, such that the get methods return fallbacks: if the asset is not present in the "local" store, then it falls back to the builtin-asset store. """ + def __init__(self): super().__init__() self.register_store(user_asset_store) diff --git a/src/Mod/CAM/Path/Tool/library/ui/editor.py b/src/Mod/CAM/Path/Tool/library/ui/editor.py index d72ee723ee..95090336f2 100644 --- a/src/Mod/CAM/Path/Tool/library/ui/editor.py +++ b/src/Mod/CAM/Path/Tool/library/ui/editor.py @@ -513,7 +513,9 @@ class LibraryEditor(object): # Create a new dictionary to hold the updated tool numbers and bits for row in range(self.toolModel.rowCount()): tool_nr_item = self.toolModel.item(row, 0) - tool_uri_item = self.toolModel.item(row, 0) # Tool URI is stored in column 0 with _PathRole + tool_uri_item = self.toolModel.item( + row, 0 + ) # Tool URI is stored in column 0 with _PathRole tool_nr = tool_nr_item.data(Qt.EditRole) tool_uri_string = tool_uri_item.data(_PathRole) @@ -530,25 +532,26 @@ class LibraryEditor(object): self.current_library.assign_new_bit_no(found_bit, int(tool_nr)) Path.Log.debug(f"Assigned tool number {tool_nr} to {tool_uri_string}") else: - Path.Log.warning(f"Toolbit with URI {tool_uri_string} not found in current library.") + Path.Log.warning( + f"Toolbit with URI {tool_uri_string} not found in current library." + ) except Exception as e: - Path.Log.error(f"Error processing row {row} (tool_nr: {tool_nr}, uri: {tool_uri_string}): {e}") + Path.Log.error( + f"Error processing row {row} (tool_nr: {tool_nr}, uri: {tool_uri_string}): {e}" + ) # Continue processing other rows even if one fails continue else: - Path.Log.warning(f"Skipping row {row}: Invalid tool number or URI.") + Path.Log.warning(f"Skipping row {row}: Invalid tool number or URI.") # The current_library object has been modified in the loop by assign_new_bit_no # Now save the modified library asset try: cam_assets.add(self.current_library) - Path.Log.debug( - f"saveLibrary: Library " f"{self.current_library.get_uri()} saved." - ) + Path.Log.debug(f"saveLibrary: Library " f"{self.current_library.get_uri()} saved.") except Exception as e: Path.Log.error( - f"saveLibrary: Failed to save library " - f"{self.current_library.get_uri()}: {e}" + f"saveLibrary: Failed to save library " f"{self.current_library.get_uri()}: {e}" ) PySide.QtGui.QMessageBox.critical( self.form,