{"record":{"id":"4c376915bd20cc97","repo":"D4Vinci/Scrapling","slug":"storage-system-must-implement-save-method","errorCode":null,"errorMessage":"Storage system must implement `save` method","messagePattern":"Storage system must implement `save` method","errorType":"exception","errorClass":"NotImplementedError","httpStatus":null,"severity":"error","filePath":"scrapling/core/storage.py","lineNumber":49,"sourceCode":"            # Fixing the inaccurate return type hint in `get_tld`\n            extracted: Result | None = cast(\n                Result, get_tld(self.url, as_object=True, fail_silently=True, fix_protocol=True)\n            )\n            if not extracted:\n                return default_value\n            return extracted.fld or extracted.domain or default_value\n        except AttributeError:\n            return default_value\n\n    @abstractmethod\n    def save(self, element: HtmlElement, identifier: str) -> None:\n        \"\"\"Saves the element's unique properties to the storage for retrieval and relocation later\n\n        :param element: The element itself which we want to save to storage.\n        :param identifier: This is the identifier that will be used to retrieve the element later from the storage. See\n            the docs for more info.\n        \"\"\"\n        raise NotImplementedError(\"Storage system must implement `save` method\")\n\n    @abstractmethod\n    def retrieve(self, identifier: str) -> Optional[Dict]:\n        \"\"\"Using the identifier, we search the storage and return the unique properties of the element\n\n        :param identifier: This is the identifier that will be used to retrieve the element from the storage. See\n            the docs for more info.\n        :return: A dictionary of the unique properties\n        \"\"\"\n        raise NotImplementedError(\"Storage system must implement `retrieve` method\")\n\n    @staticmethod\n    @lru_cache(128, typed=True)\n    def _get_hash(identifier: str) -> str:\n        \"\"\"If you want to hash identifier in your storage system, use this safer\"\"\"\n        _identifier = identifier.lower().strip()\n        # Hash functions have to take bytes\n        _identifier_bytes = _identifier.encode(\"utf-8\")","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/D4Vinci/Scrapling/blob/5d213a2d4764002bfc4fed33c32fe09fa8b0bf7f/scrapling/core/storage.py#L31-L67","documentation":"Raised by the abstract `save` method of `StorageSystemMixin` (scrapling/core/storage.py), the base class for Scrapling's adaptive element-relocation storage backends. It fires when a custom storage class is instantiated without overriding `save`, because the ABC only enforces the method's existence, not its body. The bundled `SQLiteStorageSystem` implements it, so hitting this means you subclassed `StorageSystemMixin` yourself and left `save` abstract. The `raise NotImplementedError` inside an `@abstractmethod` body is the fallback that executes if instantiation slipped through (e.g. metaclass not applied).","triggerScenarios":"Defining `class MyStorage(StorageSystemMixin)` with `retrieve` implemented but `save` missing or named differently (e.g. `save_element`, typo `savr`), then passing an instance to a Selector/adaptive feature that calls `storage.save(element, identifier)` during `element.save()` or auto-save flows.","commonSituations":"Writing a Redis/Mongo storage backend (as shown in docs/development/adaptive_storage_system.md) and forgetting one of the two required methods; renaming methods during a refactor; subclassing `SQLiteStorageSystem` and accidentally overriding `save` with a stub that calls `super().save()`.","solutions":["Implement `def save(self, element: HtmlElement, identifier: str) -> None` in your storage subclass, persisting the element's identifying properties keyed by `identifier`.","If you wanted the default behavior, use the shipped `SQLiteStorageSystem` instead of subclassing `StorageSystemMixin` directly.","Verify the subclass is fully concrete before use with `import inspect; assert not inspect.isabstract(MyStorage)`.","If wrapping the built-in storage, delegate explicitly (`SQLiteStorageSystem().save(...)`) instead of relying on the abstract base body."],"exampleFix":"// before\nclass RedisStorage(StorageSystemMixin):\n    def retrieve(self, identifier):\n        return redis.get(identifier)\n\n// after\nclass RedisStorage(StorageSystemMixin):\n    def save(self, element, identifier) -> None:\n        data = _StorageTools.element_to_dict(element)\n        redis.set(self._get_hash(identifier), json.dumps(data))\n\n    def retrieve(self, identifier):\n        return json.loads(redis.get(self._get_hash(identifier)))","handlingStrategy":"validation","validationCode":"import inspect\n\ndef make_storage(cls):\n    missing = inspect.getabstractmethods(cls) if hasattr(cls, '__abstractmethods__') else ()\n    # per-instance check\n    if getattr(MyStorage, '__abstractmethods__', None):\n        raise TypeError(f\"Storage backend incomplete: {MyStorage.__abstractmethods__}\")\n    return cls()","typeGuard":"from scrapling.core.storage import StorageSystemMixin\n\ndef is_complete_storage(s) -> bool:\n    return isinstance(s, StorageSystemMixin) and not type(s).__abstractmethods__ and hasattr(type(s).save, '__isabstractmethod__') is False and not getattr(type(s).save, '__isabstractmethod__', True)","tryCatchPattern":"try:\n    element.save(identifier='x')\nexcept NotImplementedError as e:\n    log.error(f\"Storage backend misconfigured: {e}\")  # fix the subclass, don't retry","preventionTips":["Run `assert not MyStorage.__abstractmethods__` in your storage module's tests.","Copy the method signatures (save, retrieve) exactly from StorageSystemMixin.","Model custom backends on SQLiteStorageSystem or the RedisStorage example in docs/development/adaptive_storage_system.md."],"tags":["storage","abstract-method","custom-backend","scrapling"],"backgroundTag":null,"analyzedSha":"5d213a2d4764002bfc4fed33c32fe09fa8b0bf7f","analyzedAt":"2026-08-14T22:23:09.440Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}