{"record":{"id":"a23718980fb8d7f2","repo":"p-e-w/heretic","slug":"plugin-name-does-not-export-a-class-named-cl","errorCode":null,"errorMessage":"Plugin '{name}' does not export a class named '{class_name}'","messagePattern":"Plugin '(.+?)' does not export a class named '(.+?)'","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/heretic/plugin.py","lineNumber":78,"sourceCode":") -> type[T]:\n    \"\"\"\n    Load a plugin class from either a filesystem `.py` file or a fully-qualified Python import path.\n    Also checks that the class exists in the module and that it\n    subclasses the correct Plugin subclass (e.g Scorer).\n\n    Accepted forms:\n    - `path/to/plugin.py:MyPluginClass` (relative or absolute): load `MyPluginClass`\n      from that file.\n    - `fully.qualified.module.MyPluginClass`: import the module and load the class.\n    \"\"\"\n\n    def validate_class(module: ModuleType, class_name: str) -> type[Any]:\n        \"\"\"\n        Checks that the module actually exports the class as claimed and returns the class.\n        \"\"\"\n        obj = getattr(module, class_name, None)\n        if not inspect.isclass(obj):\n            raise ValueError(\n                f\"Plugin '{name}' does not export a class named '{class_name}'\"\n            )\n        return obj\n\n    # Common user trap with filepath imports.\n    if name.endswith(\".py\"):\n        raise ValueError(\n            \"You must append the plugin class name to the filepath like this: path/to/plugin.py:ClassName\"\n        )\n\n    # File path with explicit class name, e.g. \"C:\\\\path\\\\plugin.py:MyPlugin\".\n    if \":\" in name:\n        file_path, class_name = name.rsplit(\":\", 1)\n        if not file_path.endswith(\".py\") or not class_name:\n            raise ValueError(\n                \"File-based plugin must use the form 'path/to/plugin.py:ClassName'\"\n            )\n","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/p-e-w/heretic/blob/bedb94ef117a271532ac2058447fbc165d5051bd/src/heretic/plugin.py#L60-L96","documentation":"load_plugin() resolved the plugin module but getattr(module, class_name) returned nothing class-like. The library throws this because the plugin name promises a class (after ':' for file plugins or after the last '.' for import plugins) that the module does not actually define. It guards against silently returning a function, variable, or None as a 'plugin class'.","triggerScenarios":"Calling load_plugin('path/to/plugin.py:MyScorer', Scorer) where myscorer.py defines no MyScorer; a typo in the class name segment; the class is imported under a different name (e.g. `from x import Scorer as Foo`); or an import-path plugin like 'pkg.module.Scorer' where module only exports a function or dict named Scorer.","commonSituations":"Renaming a plugin class after it was referenced in a TOML config; copy-pasting an example plugin name; the plugin file exporting the class conditionally (e.g. behind `if TYPE_CHECKING` or re-exported without __all__); case-sensitivity typos (myscorer vs MyScorer).","solutions":["Open the plugin file/module and confirm a class with the exact name (after ':' or the last '.') exists.","Fix the class name in your config/CLI argument to match the definition, including case.","If the class is defined in another module, import it into the plugin module or point load_plugin at the module that truly defines it.","If you exported a function/constant instead, wrap it in a proper subclass of the expected base class."],"exampleFix":"# before\nheretic --scorer my_scorers.py:KeywrodRate\n# after\nheretic --scorer my_scorers.py:KeywordRate","handlingStrategy":"validation","validationCode":"import importlib, inspect\ndef plugin_class_exists(dotted: str) -> bool:\n    mod_name, cls = dotted.rsplit(\".\", 1)\n    obj = getattr(importlib.import_module(mod_name), cls, None)\n    return inspect.isclass(obj)","typeGuard":"def is_plugin_class(obj: object) -> bool:\n    return inspect.isclass(obj)","tryCatchPattern":"try:\n    cls = load_plugin(name, Scorer)\nexcept ValueError as e:\n    print(f\"bad plugin name '{name}': {e}\")","preventionTips":["Keep plugin names in one config file and verify class names with grep before running.","Match case exactly; class names are case-sensitive.","Add a startup smoke test that calls load_plugin for every configured plugin."],"tags":["plugin","python","configuration"],"backgroundTag":"plugin-class-not-found","analyzedSha":"bedb94ef117a271532ac2058447fbc165d5051bd","analyzedAt":"2026-08-29T08:38:06.692Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}